20#include <rhi/qshader.h>
27#include <QApplication>
28#include <QPropertyAnimation>
31#include <QResizeEvent>
50constexpr int kUboOffsetColor = 0;
51constexpr int kUboOffsetFirstSample = 16;
52constexpr int kUboOffsetScrollSample = 20;
53constexpr int kUboOffsetSampPerPixel = 24;
54constexpr int kUboOffsetViewWidth = 28;
55constexpr int kUboOffsetViewHeight = 32;
56constexpr int kUboOffsetChannelYCenter = 36;
57constexpr int kUboOffsetChannelYRange = 40;
58constexpr int kUboOffsetAmplitudeMax = 44;
59constexpr int kUboOffsetShowClipping = 48;
62constexpr int kMaxChannels = 1024;
66constexpr float kDefaultPrefetch = 1.0f;
73static QShader loadShader(
const QString &filename)
76 if (!f.open(QIODevice::ReadOnly)) {
77 qWarning() <<
"ChannelRhiView: cannot open shader" << filename;
80 return QShader::fromSerialized(f.readAll());
83static void writeFloat(quint8 *base,
int byteOffset,
float v)
85 memcpy(base + byteOffset, &v,
sizeof(
float));
88static void writeFloats(quint8 *base,
int byteOffset,
const float *data,
int count)
90 memcpy(base + byteOffset, data, count *
sizeof(
float));
107 : QWidget(parent), m_view(parent)
109 setAttribute(Qt::WA_TransparentForMouseEvents);
110 setAttribute(Qt::WA_NoSystemBackground);
111 setAttribute(Qt::WA_TranslucentBackground);
112 setMouseTracking(
false);
115 void syncSize() { setGeometry(0, 0, parentWidget()->width(), parentWidget()->height()); }
122 p.setRenderHint(QPainter::Antialiasing,
false);
124 if (m_view->crosshairEnabled())
125 m_view->drawCrosshair(p);
126 if (m_view->scalebarsVisible())
127 m_view->drawScalebars(p);
128 if (m_view->rulerActive())
129 m_view->drawRulerOverlay(p);
130 if (m_view->annotationSelecting())
131 m_view->drawAnnotationSelectionOverlay(p);
141 setFocusPolicy(Qt::StrongFocus);
142 setMouseTracking(
true);
143 setContextMenuPolicy(Qt::PreventContextMenu);
150 connect(&m_tileWatcher, &QFutureWatcher<TileResult>::finished,
this, [
this]() {
151 m_tileRebuildPending =
false;
153 bool dirtiedDuringBuild = m_tileDirty;
154 bool tileAccepted =
false;
156 if (!m_tileWatcher.isCanceled()) {
157 TileResult r = m_tileWatcher.result();
158 if (!r.image.isNull()) {
159 m_tileImage = std::move(r.image);
160 m_tileSampleFirst = r.sampleFirst;
161 m_tileSamplesPerPixel = r.samplesPerPixel;
162 m_tileFirstChannel = r.firstChannel;
163 m_tileVisibleCount = r.visibleCount;
173 if (tileAccepted || dirtiedDuringBuild) {
174 if (dirtiedDuringBuild)
181# if defined(WASMBUILD) || defined(__EMSCRIPTEN__)
182 setApi(QRhiWidget::Api::OpenGL);
183# elif defined(Q_OS_MACOS) || defined(Q_OS_IOS)
184 setApi(QRhiWidget::Api::Metal);
185# elif defined(Q_OS_WIN)
186 setApi(QRhiWidget::Api::Direct3D11);
188 setApi(QRhiWidget::Api::OpenGL);
193 setAttribute(Qt::WA_NativeWindow);
197 connect(qApp, &QApplication::applicationStateChanged,
198 this, [
this](Qt::ApplicationState s) {
199 if (s == Qt::ApplicationActive)
212 if (m_model == model)
227 m_pipelineDirty =
true;
233 m_pipelineDirty =
true;
242 if (m_model && m_model->totalSamples() > 0)
243 sample = qMax(sample,
static_cast<float>(m_model->firstSample()));
245 sample = qMax(sample, 0.f);
248 if (m_lastFileSample >= 0) {
250 maxScroll = qMax(maxScroll,
static_cast<float>(m_firstFileSample));
251 sample = qMin(sample, maxScroll);
254 if (qFuzzyCompare(m_scrollSample, sample))
257 m_scrollSample = sample;
263 if (!m_tileImage.isNull() && m_tileSamplesPerPixel > 0.f) {
264 float vis = width() * m_samplesPerPixel;
265 float tileEnd = m_tileSampleFirst + m_tileImage.width() * m_tileSamplesPerPixel;
266 if (m_scrollSample < m_tileSampleFirst + vis ||
267 m_scrollSample + vis > tileEnd - vis)
272 float visible = width() * m_samplesPerPixel;
273 float margin = m_prefetchFactor * visible;
274 if (sample < m_vboWindowFirst + margin ||
275 sample + visible > m_vboWindowLast - margin) {
282 if (m_overlayTotalSamples <= 0.f ||
283 sample < m_overlayFirstSample ||
284 sample + visible > m_overlayFirstSample + m_overlayTotalSamples) {
285 m_overlayDirty =
true;
296 spp = qMax(spp, 1e-4f);
297 if (qFuzzyCompare(m_samplesPerPixel, spp))
299 m_samplesPerPixel = spp;
301 m_overlayDirty =
true;
311 if (durationMs <= 0) {
315 auto *anim =
new QPropertyAnimation(
this,
"scrollSample",
this);
316 anim->setDuration(durationMs);
317 anim->setEasingCurve(QEasingCurve::OutCubic);
318 anim->setStartValue(m_scrollSample);
319 anim->setEndValue(targetSample);
320 anim->start(QAbstractAnimation::DeleteWhenStopped);
327 targetSpp = qMax(targetSpp, 1e-4f);
328 if (durationMs <= 0) {
332 auto *anim =
new QPropertyAnimation(
this,
"samplesPerPixel",
this);
333 anim->setDuration(durationMs);
334 anim->setEasingCurve(QEasingCurve::OutCubic);
335 anim->setStartValue(m_samplesPerPixel);
336 anim->setEndValue(targetSpp);
337 anim->start(QAbstractAnimation::DeleteWhenStopped);
346 m_overlayDirty =
true;
354 m_prefetchFactor = qMax(factor, 0.1f);
361 return static_cast<int>(m_scrollSample);
368 return static_cast<int>(width() * m_samplesPerPixel);
376 ch = qBound(0, ch, maxFirst);
377 if (ch == m_firstVisibleChannel)
379 m_firstVisibleChannel = ch;
382 m_pipelineDirty =
true;
391 count = qMax(1, count);
392 if (count == m_visibleChannelCount)
394 m_visibleChannelCount = count;
397 m_pipelineDirty =
true;
406 if (m_frozen && m_pInertialAnim) {
407 m_pInertialAnim->stop();
408 m_pInertialAnim =
nullptr;
416 if (visible == m_gridVisible)
418 m_gridVisible = visible;
420 m_overlayDirty =
true;
428 m_sfreq = qMax(sfreq, 0.f);
430 m_overlayDirty =
true;
438 if (first == m_firstFileSample)
440 m_firstFileSample = first;
442 m_overlayDirty =
true;
450 m_lastFileSample = last;
457 if (m_hideBadChannels == hide)
460 const int previousFirstVisibleChannel = m_firstVisibleChannel;
461 m_hideBadChannels = hide;
463 m_firstVisibleChannel = qBound(0, m_firstVisibleChannel, maxFirst);
464 if (m_firstVisibleChannel != previousFirstVisibleChannel) {
468 m_pipelineDirty =
true;
477 m_wheelScrollsChannels = channelsMode;
484 m_scrollSpeedFactor = qBound(0.25f, factor, 4.0f);
491 if (m_crosshairEnabled == enabled)
493 m_crosshairEnabled = enabled;
495 setMouseTracking(
true);
497 setMouseTracking(
false);
498 m_crosshairX = m_crosshairY = -1;
507 if (m_scalebarsVisible == visible)
509 m_scalebarsVisible = visible;
517 if (m_butterflyMode == enabled)
519 m_butterflyMode = enabled;
521 m_pipelineDirty =
true;
523 m_overlayDirty =
true;
529QVector<ChannelRhiView::ButterflyTypeGroup> ChannelRhiView::butterflyTypeGroups()
const
531 QVector<ButterflyTypeGroup> groups;
535 const QVector<int> allCh = effectiveChannelIndices();
536 QMap<QString, int> typeToGroup;
538 for (
int ch : allCh) {
539 auto info = m_model->channelInfo(ch);
540 if (m_hideBadChannels && info.bad)
543 if (typeToGroup.contains(info.typeLabel)) {
544 gIdx = typeToGroup[info.typeLabel];
546 gIdx = groups.size();
547 typeToGroup[info.typeLabel] = gIdx;
548 ButterflyTypeGroup g;
549 g.typeLabel = info.typeLabel;
550 g.color = info.color;
551 g.amplitudeMax = info.amplitudeMax;
554 groups[gIdx].channelIndices.append(ch);
561int ChannelRhiView::butterflyLaneCount()
const
565 const QVector<int> allCh = effectiveChannelIndices();
567 for (
int ch : allCh) {
568 auto info = m_model->channelInfo(ch);
569 if (m_hideBadChannels && info.bad)
571 types.insert(info.typeLabel);
580 const int previousFirstVisibleChannel = m_firstVisibleChannel;
581 m_filteredChannels = indices;
584 m_firstVisibleChannel = qBound(0, m_firstVisibleChannel, maxFirst);
585 if (m_firstVisibleChannel != previousFirstVisibleChannel) {
589 m_pipelineDirty =
true;
598 return effectiveChannelIndices().size();
603int ChannelRhiView::actualChannelAt(
int logicalIdx)
const
605 const QVector<int> indices = effectiveChannelIndices();
606 if (logicalIdx < 0 || logicalIdx >= indices.size())
608 return indices.at(logicalIdx);
613QVector<int> ChannelRhiView::effectiveChannelIndices()
const
615 QVector<int> indices;
621 if (m_filteredChannels.isEmpty()) {
622 indices.reserve(m_model->channelCount());
623 for (
int channelIndex = 0; channelIndex < m_model->channelCount(); ++channelIndex) {
624 indices.append(channelIndex);
627 indices = m_filteredChannels;
630 if (!m_hideBadChannels) {
634 QVector<int> visibleIndices;
635 visibleIndices.reserve(indices.size());
636 for (
int channelIndex : std::as_const(indices)) {
637 if (channelIndex < 0) {
641 const ChannelDisplayInfo info = m_model->channelInfo(channelIndex);
643 visibleIndices.append(channelIndex);
647 return visibleIndices;
656 m_overlayDirty =
true;
664 m_epochTriggerSamples = triggerSamples;
666 m_overlayDirty =
true;
674 if (m_bShowEpochMarkers == visible)
676 m_bShowEpochMarkers = visible;
678 m_overlayDirty =
true;
686 if (m_bShowClipping == visible)
688 m_bShowClipping = visible;
697 if (m_bZScoreMode == enabled)
699 m_bZScoreMode = enabled;
709 m_annotations = annotations;
711 m_overlayDirty =
true;
719 m_annotationSelectionEnabled = enabled;
726 if (m_bShowEvents == visible)
return;
727 m_bShowEvents = visible;
729 m_overlayDirty =
true;
739 if (m_bShowAnnotations == visible)
return;
740 m_bShowAnnotations = visible;
742 m_overlayDirty =
true;
750int ChannelRhiView::hitTestAnnotationBoundary(
int px,
bool &isStart)
const
752 for (
int i = 0; i < m_annotations.size(); ++i) {
753 const float xStart = (
static_cast<float>(m_annotations[i].startSample) - m_scrollSample) / m_samplesPerPixel;
754 const float xEnd = (
static_cast<float>(m_annotations[i].endSample + 1) - m_scrollSample) / m_samplesPerPixel;
756 if (qAbs(px -
static_cast<int>(xStart)) <= kAnnBoundaryHitPx) {
760 if (qAbs(px -
static_cast<int>(xEnd)) <= kAnnBoundaryHitPx) {
772 m_pipelineDirty =
true;
783 m_gpuChannels.clear();
784 m_pipelineDirty =
true;
787 m_overlayPipeline.reset();
788 m_overlaySrb.reset();
789 m_overlaySampler.reset();
790 m_overlayTex.reset();
791 m_overlayVbo.reset();
792 m_overlayDirty =
true;
797void ChannelRhiView::ensurePipeline()
799 if (!m_pipelineDirty)
802 QRhi *rhi = this->rhi();
806 m_uboStride =
static_cast<int>(
807 (52 + rhi->ubufAlignment() - 1) & ~(rhi->ubufAlignment() - 1));
813 if (m_butterflyMode) {
814 nCh = qMin(totalCh, kMaxChannels);
816 nCh = qMin(m_visibleChannelCount, totalCh - m_firstVisibleChannel);
819 nCh = qMin(nCh, kMaxChannels);
822 bool uboRecreated =
false;
823 if (!m_ubo || m_ubo->size() < nCh * m_uboStride) {
824 m_ubo.reset(rhi->newBuffer(QRhiBuffer::Dynamic,
825 QRhiBuffer::UniformBuffer,
833 if (!m_srb || uboRecreated) {
834 m_srb.reset(rhi->newShaderResourceBindings());
836 QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(
838 QRhiShaderResourceBinding::VertexStage |
839 QRhiShaderResourceBinding::FragmentStage,
849 QShader vs = loadShader(QStringLiteral(
":/disp/shaders/viewers/helpers/shaders/channeldata.vert.qsb"));
850 QShader fs = loadShader(QStringLiteral(
":/disp/shaders/viewers/helpers/shaders/channeldata.frag.qsb"));
852 if (!vs.isValid() || !fs.isValid()) {
853 qWarning() <<
"ChannelRhiView: shaders not found. "
854 "Ensure qt_add_shaders is configured in CMakeLists.";
861 m_pipeline.reset(rhi->newGraphicsPipeline());
862 m_pipeline->setShaderStages({
863 { QRhiShaderStage::Vertex, vs },
864 { QRhiShaderStage::Fragment, fs }
867 QRhiVertexInputLayout il;
868 il.setBindings({{ 2 *
sizeof(float) }});
869 il.setAttributes({{ 0, 0, QRhiVertexInputAttribute::Float2, 0 }});
871 m_pipeline->setVertexInputLayout(il);
872 m_pipeline->setShaderResourceBindings(m_srb.get());
873 m_pipeline->setRenderPassDescriptor(renderTarget()->renderPassDescriptor());
874 m_pipeline->setTopology(QRhiGraphicsPipeline::LineStrip);
875 m_pipeline->setDepthTest(
false);
876 m_pipeline->setDepthWrite(
false);
879 QRhiGraphicsPipeline::TargetBlend blend;
881 blend.srcColor = QRhiGraphicsPipeline::SrcAlpha;
882 blend.dstColor = QRhiGraphicsPipeline::OneMinusSrcAlpha;
883 blend.srcAlpha = QRhiGraphicsPipeline::One;
884 blend.dstAlpha = QRhiGraphicsPipeline::OneMinusSrcAlpha;
885 m_pipeline->setTargetBlends({ blend });
887 if (!m_pipeline->create()) {
888 qWarning() <<
"ChannelRhiView: failed to create graphics pipeline";
893 m_pipelineDirty =
false;
898bool ChannelRhiView::isVboDirty()
const
902 float visible = width() * m_samplesPerPixel;
903 float margin = m_prefetchFactor * visible;
905 || m_scrollSample < m_vboWindowFirst + margin
906 || (m_scrollSample + visible) > m_vboWindowLast - margin;
911void ChannelRhiView::rebuildVBOs(QRhiResourceUpdateBatch *batch)
916 QRhi *rhi = this->rhi();
922 float visible = px * m_samplesPerPixel;
925 float windowFirst = m_scrollSample - m_prefetchFactor * visible;
926 float windowLast = m_scrollSample + (1.f + m_prefetchFactor) * visible;
928 int iFirst = qMax(
static_cast<int>(windowFirst), m_model->firstSample());
929 int iLast = qMin(
static_cast<int>(windowLast),
930 m_model->firstSample() + m_model->totalSamples());
931 if (iFirst >= iLast) {
936 m_vboWindowFirst = iFirst;
937 m_vboWindowLast = iLast;
940 m_gpuChannels.resize(nCh);
943 int prefetchedSamples = iLast - iFirst;
946 int maxVertices = qMax(prefetchedSamples * 2, 2 * px * 4);
947 Q_UNUSED(maxVertices)
949 for (
int logCh = 0; logCh < nCh; ++logCh) {
950 int ch = actualChannelAt(logCh);
952 m_gpuChannels[logCh].vertexCount = 0;
956 QVector<float> verts = m_model->decimatedVertices(
957 ch, iFirst, iLast,
static_cast<int>(prefetchedSamples / m_samplesPerPixel), vboFirst);
959 if (verts.isEmpty()) {
960 m_gpuChannels[logCh].vertexCount = 0;
966 int nv = verts.size() / 2;
968 double sum = 0.0, sumSq = 0.0;
969 for (
int v = 0; v < nv; ++v) {
970 double a =
static_cast<double>(verts[v * 2 + 1]);
974 float mean =
static_cast<float>(sum / nv);
975 double var = sumSq / nv -
static_cast<double>(mean) * mean;
976 float sd = var > 0.0 ?
static_cast<float>(qSqrt(var)) : 1.f;
977 for (
int v = 0; v < nv; ++v)
978 verts[v * 2 + 1] = (verts[v * 2 + 1] - mean) / sd;
982 int vertexCount = verts.size() / 2;
983 quint32 byteSize =
static_cast<quint32
>(verts.size() *
sizeof(
float));
985 auto &gd = m_gpuChannels[logCh];
988 if (!gd.vbo ||
static_cast<quint32
>(gd.vbo->size()) < byteSize) {
989 gd.vbo.reset(rhi->newBuffer(QRhiBuffer::Dynamic,
990 QRhiBuffer::VertexBuffer,
992 if (!gd.vbo->create()) {
993 qWarning() <<
"ChannelRhiView: VBO create failed for channel" << logCh;
999 batch->updateDynamicBuffer(gd.vbo.get(), 0, byteSize,
1001 gd.vertexCount = vertexCount;
1002 gd.vboFirstSample = vboFirst;
1010void ChannelRhiView::updateUBO(QRhiResourceUpdateBatch *batch)
1012 if (!m_model || !m_ubo)
1018 if (m_butterflyMode) {
1019 const auto groups = butterflyTypeGroups();
1020 int nLanes = groups.size();
1025 QHash<int, int> chToLane;
1026 for (
int g = 0; g < groups.size(); ++g)
1027 for (
int ch : groups[g].channelIndices)
1030 float vw =
static_cast<float>(width());
1031 float vh =
static_cast<float>(height());
1032 float laneRange = 2.f / nLanes;
1034 QVarLengthArray<quint8> buf(m_uboStride, 0);
1035 int nToUpload = qMin(totalCh, kMaxChannels);
1037 for (
int logCh = 0; logCh < nToUpload; ++logCh) {
1038 int ch = actualChannelAt(logCh);
1039 memset(buf.data(), 0, m_uboStride);
1041 auto info = (ch >= 0) ? m_model->channelInfo(ch) : ChannelDisplayInfo{};
1042 bool hideThis = (ch < 0) || (m_hideBadChannels && info.
bad);
1044 int lane = chToLane.value(ch, -1);
1048 QColor col = hideThis ? m_bgColor
1049 : (info.
bad ? QColor(200, 60, 60, 180) : info.color);
1050 float yRng = hideThis ? 0.f : laneRange;
1052 float yCenter = (lane >= 0)
1053 ? (1.f - laneRange * (lane + 0.5f))
1057 static_cast<float>(col.redF()),
1058 static_cast<float>(col.greenF()),
1059 static_cast<float>(col.blueF()),
1060 static_cast<float>(col.alphaF())
1063 auto *d = buf.data();
1064 writeFloats(d, kUboOffsetColor, rgba, 4);
1065 writeFloat (d, kUboOffsetFirstSample,
static_cast<float>(logCh <
static_cast<int>(m_gpuChannels.size())
1066 ? m_gpuChannels[logCh].vboFirstSample : 0));
1067 writeFloat (d, kUboOffsetScrollSample, m_scrollSample);
1068 writeFloat (d, kUboOffsetSampPerPixel, m_samplesPerPixel);
1069 writeFloat (d, kUboOffsetViewWidth, vw);
1070 writeFloat (d, kUboOffsetViewHeight, vh);
1071 writeFloat (d, kUboOffsetChannelYCenter, yCenter);
1072 writeFloat (d, kUboOffsetChannelYRange, yRng);
1073 writeFloat (d, kUboOffsetAmplitudeMax, m_bZScoreMode ? 4.f : info.
amplitudeMax);
1074 writeFloat (d, kUboOffsetShowClipping, (m_bShowClipping && !info.
bad && !m_bZScoreMode) ? 1.f : 0.f);
1076 batch->updateDynamicBuffer(m_ubo.get(),
1077 logCh * m_uboStride,
1085 int firstCh = qBound(0, m_firstVisibleChannel, totalCh);
1086 int visCnt = qMin(m_visibleChannelCount, totalCh - firstCh);
1087 int nCh = qMin(visCnt, kMaxChannels);
1091 float vw =
static_cast<float>(width());
1092 float vh =
static_cast<float>(height());
1093 float laneRange = 2.f / nCh;
1095 QVarLengthArray<quint8> buf(m_uboStride, 0);
1097 for (
int i = 0; i < nCh; ++i) {
1098 int logCh = firstCh + i;
1099 int ch = actualChannelAt(logCh);
1100 memset(buf.data(), 0, m_uboStride);
1102 auto info = (ch >= 0) ? m_model->channelInfo(ch) : ChannelDisplayInfo{};
1103 bool hideThis = (ch < 0) || (m_hideBadChannels && info.
bad);
1105 QColor col = hideThis ? m_bgColor
1106 : (info.
bad ? QColor(200, 60, 60, 180) : info.color);
1108 float yRng = hideThis ? 0.f : laneRange;
1111 static_cast<float>(col.redF()),
1112 static_cast<float>(col.greenF()),
1113 static_cast<float>(col.blueF()),
1114 static_cast<float>(col.alphaF())
1118 float yCenter = 1.f - laneRange * (i + 0.5f);
1120 auto *d = buf.data();
1121 writeFloats(d, kUboOffsetColor, rgba, 4);
1123 writeFloat (d, kUboOffsetFirstSample,
static_cast<float>(logCh <
static_cast<int>(m_gpuChannels.size())
1124 ? m_gpuChannels[logCh].vboFirstSample : 0));
1125 writeFloat (d, kUboOffsetScrollSample, m_scrollSample);
1126 writeFloat (d, kUboOffsetSampPerPixel, m_samplesPerPixel);
1127 writeFloat (d, kUboOffsetViewWidth, vw);
1128 writeFloat (d, kUboOffsetViewHeight, vh);
1129 writeFloat (d, kUboOffsetChannelYCenter, yCenter);
1130 writeFloat (d, kUboOffsetChannelYRange, yRng);
1131 writeFloat (d, kUboOffsetAmplitudeMax, m_bZScoreMode ? 4.f : info.
amplitudeMax);
1132 writeFloat (d, kUboOffsetShowClipping, (m_bShowClipping && !info.
bad && !m_bZScoreMode) ? 1.f : 0.f);
1135 batch->updateDynamicBuffer(m_ubo.get(),
1153void ChannelRhiView::rebuildOverlayImage(
int logicalWidth,
int logicalHeight, qreal devicePixelRatio)
1155 const qreal dpr = qMax(devicePixelRatio, 1.0);
1156 const int pixelWidth = qMax(1, qRound(logicalWidth * dpr));
1157 const int pixelHeight = qMax(1, qRound(logicalHeight * dpr));
1159 m_overlayImage = QImage(pixelWidth, pixelHeight, QImage::Format_RGBA8888);
1160 m_overlayImage.setDevicePixelRatio(dpr);
1161 m_overlayImage.fill(Qt::transparent);
1163 if (logicalWidth <= 0 || logicalHeight <= 0 || m_sfreq <= 0.f || m_samplesPerPixel <= 0.f) {
1164 m_overlayDirty =
false;
1170 const float overlayFirst = m_overlayFirstSample;
1171 const float overlayTotal = m_overlayTotalSamples;
1172 const float overlayPixelsPerSample = (overlayTotal > 0.f)
1173 ?
static_cast<float>(logicalWidth) / overlayTotal
1176 QPainter p(&m_overlayImage);
1177 p.setCompositionMode(QPainter::CompositionMode_SourceOver);
1183 if (m_bShowAnnotations && !m_annotations.isEmpty()) {
1184 QFont font = p.font();
1185 font.setPointSizeF(8.0);
1190 const float xStart = (
static_cast<float>(annotation.startSample) - overlayFirst) * overlayPixelsPerSample;
1191 const float xEnd = (
static_cast<float>(annotation.endSample + 1) - overlayFirst) * overlayPixelsPerSample;
1192 if (xEnd < -2.f || xStart > logicalWidth + 2.f) {
1196 const float clippedStart = qBound(0.f, xStart,
static_cast<float>(logicalWidth));
1197 const float clippedEnd = qBound(0.f, xEnd,
static_cast<float>(logicalWidth));
1198 if (clippedEnd <= clippedStart) {
1202 QColor fillColor = annotation.color;
1203 fillColor.setAlpha(48);
1204 p.fillRect(QRectF(clippedStart, 0.f, clippedEnd - clippedStart,
static_cast<float>(logicalHeight)),
1207 QColor borderColor = annotation.color;
1208 borderColor.setAlpha(165);
1209 p.setPen(QPen(borderColor, 1));
1210 p.drawLine(QPointF(clippedStart, 0.f), QPointF(clippedStart,
static_cast<float>(logicalHeight)));
1211 p.drawLine(QPointF(clippedEnd, 0.f), QPointF(clippedEnd,
static_cast<float>(logicalHeight)));
1213 if (!annotation.label.trimmed().isEmpty()) {
1214 QString label = annotation.label.trimmed();
1215 QFontMetrics metrics(font);
1216 QRect labelRect = metrics.boundingRect(label);
1217 labelRect.adjust(-6, -2, 6, 2);
1218 const int labelX = qBound(4,
1219 static_cast<int>(clippedStart) + 4,
1220 qMax(4, logicalWidth - labelRect.width() - 4));
1221 labelRect.moveTopLeft(QPoint(labelX, 4));
1222 QColor pillColor = annotation.color;
1223 pillColor.setAlpha(215);
1224 p.fillRect(labelRect, pillColor);
1225 p.setPen(Qt::white);
1226 p.drawText(labelRect, Qt::AlignCenter, label);
1232 if (m_bShowEvents && !m_events.isEmpty()) {
1234 float xF = (
static_cast<float>(ev.sample) - overlayFirst) * overlayPixelsPerSample;
1235 if (xF < -2.f || xF > logicalWidth + 2.f)
1237 QColor lineColor = ev.color;
1238 lineColor.setAlpha(180);
1239 p.setPen(QPen(lineColor, 1));
1240 p.drawLine(QPointF(xF, 0.f), QPointF(xF,
static_cast<float>(logicalHeight)));
1245 if (m_bShowEpochMarkers && !m_epochTriggerSamples.isEmpty()) {
1246 QPen epochPen(QColor(100, 100, 100, 140), 1, Qt::DashLine);
1248 for (
int trigSample : m_epochTriggerSamples) {
1249 float xF = (
static_cast<float>(trigSample) - overlayFirst) * overlayPixelsPerSample;
1250 if (xF < -2.f || xF > logicalWidth + 2.f)
1252 p.drawLine(QPointF(xF, 0.f), QPointF(xF,
static_cast<float>(logicalHeight)));
1256 m_overlayDirty =
false;
1261void ChannelRhiView::ensureOverlayPipeline()
1263 QRhi *rhi = this->rhi();
1264 if (!rhi || !renderTarget())
1266 if (m_overlayPipeline)
1276 static constexpr float kQuadVerts[] = {
1277 -1.f, -1.f, 0.f, 1.f,
1278 -1.f, 1.f, 0.f, 0.f,
1279 1.f, -1.f, 1.f, 1.f,
1282 static constexpr int kQuadBytes =
sizeof(kQuadVerts);
1283 m_overlayVbo.reset(rhi->newBuffer(QRhiBuffer::Immutable,
1284 QRhiBuffer::VertexBuffer,
1286 if (!m_overlayVbo->create()) {
1287 m_overlayVbo.reset();
1292 m_overlayTex.reset(rhi->newTexture(QRhiTexture::RGBA8, QSize(1, 1)));
1293 m_overlayTex->create();
1295 m_overlaySampler.reset(rhi->newSampler(
1296 QRhiSampler::Linear, QRhiSampler::Linear,
1298 QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge));
1299 m_overlaySampler->create();
1303 static constexpr int kOverlayUboSize = 256;
1304 m_overlayUbo.reset(rhi->newBuffer(QRhiBuffer::Dynamic,
1305 QRhiBuffer::UniformBuffer,
1307 if (!m_overlayUbo->create()) {
1308 m_overlayVbo.reset();
1309 m_overlayUbo.reset();
1314 m_overlaySrb.reset(rhi->newShaderResourceBindings());
1315 m_overlaySrb->setBindings({
1316 QRhiShaderResourceBinding::sampledTexture(
1317 1, QRhiShaderResourceBinding::FragmentStage,
1318 m_overlayTex.get(), m_overlaySampler.get()),
1319 QRhiShaderResourceBinding::uniformBuffer(
1320 2, QRhiShaderResourceBinding::FragmentStage,
1323 m_overlaySrb->create();
1325 auto loadShader = [](
const QString &path) -> QShader {
1327 if (!f.open(QIODevice::ReadOnly)) {
1328 qWarning() <<
"ChannelRhiView: cannot open shader" << path;
1331 return QShader::fromSerialized(f.readAll());
1334 QShader vs = loadShader(QStringLiteral(
":/disp/shaders/viewers/helpers/shaders/overlay.vert.qsb"));
1335 QShader fs = loadShader(QStringLiteral(
":/disp/shaders/viewers/helpers/shaders/overlay.frag.qsb"));
1336 if (!vs.isValid() || !fs.isValid()) {
1337 m_overlayVbo.reset();
1338 m_overlayUbo.reset();
1339 m_overlaySrb.reset();
1340 m_overlaySampler.reset();
1341 m_overlayTex.reset();
1345 m_overlayPipeline.reset(rhi->newGraphicsPipeline());
1346 QRhiGraphicsPipeline::TargetBlend blend;
1347 blend.enable =
true;
1348 blend.srcColor = QRhiGraphicsPipeline::SrcAlpha;
1349 blend.dstColor = QRhiGraphicsPipeline::OneMinusSrcAlpha;
1350 blend.srcAlpha = QRhiGraphicsPipeline::One;
1351 blend.dstAlpha = QRhiGraphicsPipeline::OneMinusSrcAlpha;
1352 m_overlayPipeline->setTargetBlends({ blend });
1353 m_overlayPipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip);
1354 m_overlayPipeline->setDepthTest(
false);
1355 m_overlayPipeline->setDepthWrite(
false);
1356 m_overlayPipeline->setShaderStages({
1357 { QRhiShaderStage::Vertex, vs },
1358 { QRhiShaderStage::Fragment, fs },
1361 QRhiVertexInputLayout inputLayout;
1362 inputLayout.setBindings({ QRhiVertexInputBinding(4 *
sizeof(
float)) });
1363 inputLayout.setAttributes({
1364 QRhiVertexInputAttribute(0, 0, QRhiVertexInputAttribute::Float2, 0),
1365 QRhiVertexInputAttribute(0, 1, QRhiVertexInputAttribute::Float2, 2 *
sizeof(
float))
1367 m_overlayPipeline->setVertexInputLayout(inputLayout);
1368 m_overlayPipeline->setShaderResourceBindings(m_overlaySrb.get());
1369 m_overlayPipeline->setRenderPassDescriptor(renderTarget()->renderPassDescriptor());
1371 if (!m_overlayPipeline->create()) {
1372 qWarning() <<
"ChannelRhiView: overlay pipeline create failed";
1373 m_overlayPipeline.reset();
1380 m_overlayVboNeedsUpload =
true;
1389 QRhiResourceUpdateBatch *u = rhi()->nextResourceUpdateBatch();
1390 QColor bg = m_bgColor;
1391 cb->beginPass(renderTarget(), bg, {1.f, 0}, u);
1400 QRhiResourceUpdateBatch *u = rhi()->nextResourceUpdateBatch();
1401 cb->beginPass(renderTarget(), QColor(220, 0, 0), {1.f, 0}, u);
1406 QSize ps = renderTarget()->pixelSize();
1407 const int pw = ps.width();
1408 const int ph = ps.height();
1409 const int logicalW = width();
1410 const int logicalH = height();
1411 const qreal overlayDpr = (logicalW > 0) ? (
static_cast<qreal
>(pw) /
static_cast<qreal
>(logicalW)) : 1.0;
1413 QRhiResourceUpdateBatch *batch = rhi()->nextResourceUpdateBatch();
1421 ensureOverlayPipeline();
1422 bool overlayReady =
false;
1423 if (m_overlayPipeline && m_overlayVbo && m_overlayUbo && pw > 0 && ph > 0 && logicalW > 0 && logicalH > 0) {
1426 if (m_overlayVboNeedsUpload) {
1427 static constexpr float kQuadVerts[] = {
1428 -1.f, -1.f, 0.f, 1.f,
1429 -1.f, 1.f, 0.f, 0.f,
1430 1.f, -1.f, 1.f, 1.f,
1433 batch->uploadStaticBuffer(m_overlayVbo.get(), 0,
1434 static_cast<quint32
>(
sizeof(kQuadVerts)), kQuadVerts);
1435 m_overlayVboNeedsUpload =
false;
1439 const float visibleSamples =
static_cast<float>(logicalW) * m_samplesPerPixel;
1440 const float extraSamples = kOverlayPrefetchFactor * visibleSamples;
1441 const float overlayFirstSample = m_scrollSample - extraSamples;
1442 const float overlayTotalSamples = visibleSamples + 2.0f * extraSamples;
1444 const int overlayLogicalW =
static_cast<int>(std::ceil(
1445 (1.0f + 2.0f * kOverlayPrefetchFactor) *
static_cast<float>(logicalW)));
1448 const QSize requiredTexSize(qRound(overlayLogicalW * overlayDpr),
1449 qRound(logicalH * overlayDpr));
1450 if (m_overlayDirty || requiredTexSize != m_overlayTexSize) {
1452 m_overlayFirstSample = overlayFirstSample;
1453 m_overlayTotalSamples = overlayTotalSamples;
1455 rebuildOverlayImage(overlayLogicalW, logicalH, overlayDpr);
1458 if (requiredTexSize != m_overlayTexSize) {
1459 m_overlayTex.reset(rhi()->newTexture(QRhiTexture::RGBA8, requiredTexSize));
1460 m_overlayTex->create();
1462 m_overlaySrb->setBindings({
1463 QRhiShaderResourceBinding::sampledTexture(
1464 1, QRhiShaderResourceBinding::FragmentStage,
1465 m_overlayTex.get(), m_overlaySampler.get()),
1466 QRhiShaderResourceBinding::uniformBuffer(
1467 2, QRhiShaderResourceBinding::FragmentStage,
1470 m_overlaySrb->create();
1471 m_overlayTexSize = requiredTexSize;
1473 QRhiTextureUploadEntry entry(0, 0, QRhiTextureSubresourceUploadDescription(m_overlayImage));
1474 batch->uploadTexture(m_overlayTex.get(), entry);
1478 struct OverlayParams {
1483 float firstFileSample;
1485 float overlayFirstSample;
1486 float overlayTotalSamples;
1488 OverlayParams params;
1489 params.scrollSample = m_scrollSample;
1490 params.samplesPerPixel = m_samplesPerPixel;
1491 params.viewWidth =
static_cast<float>(logicalW);
1492 params.sfreq = m_sfreq;
1493 params.firstFileSample =
static_cast<float>(m_firstFileSample);
1494 params.gridEnabled = m_gridVisible ? 1.0f : 0.0f;
1495 params.overlayFirstSample = m_overlayFirstSample;
1496 params.overlayTotalSamples = m_overlayTotalSamples;
1497 batch->updateDynamicBuffer(m_overlayUbo.get(), 0,
1498 static_cast<quint32
>(
sizeof(params)), ¶ms);
1500 overlayReady =
true;
1504 QColor bg = m_bgColor;
1505 cb->beginPass(renderTarget(), bg, {1.f, 0}, batch);
1507 cb->setViewport(QRhiViewport(0.f, 0.f,
static_cast<float>(pw),
1508 static_cast<float>(ph)));
1511 cb->setGraphicsPipeline(m_pipeline.get());
1515 if (m_butterflyMode) {
1517 int nToRender = qMin(totalCh, kMaxChannels);
1518 for (
int logCh = 0; logCh < nToRender; ++logCh) {
1519 if (logCh >=
static_cast<int>(m_gpuChannels.size()))
1521 auto &gd = m_gpuChannels[logCh];
1522 if (!gd.vbo || gd.vertexCount < 2)
1525 quint32 dynOffset =
static_cast<quint32
>(logCh * m_uboStride);
1526 QRhiCommandBuffer::DynamicOffset dynOff{0, dynOffset};
1527 cb->setShaderResources(m_srb.get(), 1, &dynOff);
1529 QRhiCommandBuffer::VertexInput vi(gd.vbo.get(), 0);
1530 cb->setVertexInput(0, 1, &vi);
1531 cb->draw(
static_cast<quint32
>(gd.vertexCount));
1535 int firstCh = qBound(0, m_firstVisibleChannel, totalCh);
1536 int visCnt = qMin(m_visibleChannelCount, totalCh - firstCh);
1537 int nToRender = qMin(visCnt, kMaxChannels);
1539 for (
int i = 0; i < nToRender; ++i) {
1540 int logCh = firstCh + i;
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
>(i * 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));
1559 cb->setGraphicsPipeline(m_overlayPipeline.get());
1560 cb->setShaderResources(m_overlaySrb.get());
1561 QRhiCommandBuffer::VertexInput overlayVi(m_overlayVbo.get(), 0);
1562 cb->setVertexInput(0, 1, &overlayVi);
1571 QRhiWidget::paintEvent(event);
1579bool ChannelRhiView::isTileFresh()
const
1581 if (m_tileDirty || m_tileImage.isNull() || m_tileSamplesPerPixel <= 0.f)
1583 if (!qFuzzyCompare(m_tileSamplesPerPixel, m_samplesPerPixel))
1585 if (m_tileFirstChannel != m_firstVisibleChannel)
1588 int visibleCount = qMin(m_visibleChannelCount, totalCh - m_firstVisibleChannel);
1589 if (m_tileVisibleCount != visibleCount)
1593 float visibleSamples = width() * m_samplesPerPixel;
1594 float tileEnd = m_tileSampleFirst + m_tileImage.width() * m_tileSamplesPerPixel;
1595 if (m_scrollSample < m_tileSampleFirst + visibleSamples)
1597 if (m_scrollSample + visibleSamples > tileEnd - visibleSamples)
1605void ChannelRhiView::scheduleTileRebuild()
1608 if (m_tileRebuildPending)
1615 m_tileImage = QImage(qMax(width(), 1), qMax(height(), 1), QImage::Format_RGB32);
1616 m_tileImage.fill(m_bgColor.rgb());
1617 m_tileSampleFirst = m_scrollSample;
1618 m_tileSamplesPerPixel = qMax(m_samplesPerPixel, 1e-4f);
1619 m_tileFirstChannel = m_firstVisibleChannel;
1620 m_tileVisibleCount = 0;
1621 m_tileDirty =
false;
1626 ChannelDataModel *model = m_model.data();
1628 float spp = m_samplesPerPixel;
1629 int firstCh = m_firstVisibleChannel;
1630 int visCnt = m_visibleChannelCount;
1633 QColor bg = m_bgColor;
1634 bool gridVis = m_gridVisible;
1635 float sfreq = m_sfreq;
1636 int firstFileSample = m_firstFileSample;
1637 bool hideBad = m_hideBadChannels;
1638 QVector<int> chIndices = m_filteredChannels;
1639 QVector<EventMarker> eventsSnap = m_bShowEvents ? m_events : QVector<EventMarker>();
1640 QVector<AnnotationSpan> annotationsSnap = m_bShowAnnotations ? m_annotations : QVector<AnnotationSpan>();
1641 QVector<int> epochSnap = m_bShowEpochMarkers ? m_epochTriggerSamples : QVector<int>();
1642 bool clipSnap = m_bShowClipping;
1643 bool zscoreSnap = m_bZScoreMode;
1645 m_tileDirty =
false;
1646 m_tileRebuildPending =
true;
1647 m_tileWatcher.setFuture(QtConcurrent::run([=]() {
1648 return ChannelRhiView::buildTile(model,
scrollSample, spp, firstCh, visCnt,
1649 pw, ph, bg, gridVis, sfreq, firstFileSample,
1650 hideBad, chIndices, eventsSnap, annotationsSnap, epochSnap,
1651 clipSnap, zscoreSnap);
1657ChannelRhiView::TileResult ChannelRhiView::buildTile(
1659 float scrollSample,
float spp,
1660 int firstCh,
int visCnt,
1662 QColor bgColor,
bool gridVisible,
1663 float sfreq,
int firstFileSample,
1664 bool hideBadChannels,
1665 const QVector<int> &channelIndices,
1666 const QVector<EventMarker> &events,
1667 const QVector<AnnotationSpan> &annotations,
1668 const QVector<int> &epochMarkers,
1673 out.samplesPerPixel = spp;
1674 out.firstChannel = firstCh;
1676 if (!model || pw <= 0 || ph <= 0 || spp <= 0.f)
1679 int totalCh = channelIndices.isEmpty() ? model->
channelCount() : channelIndices.size();
1680 int visibleCount = qMin(visCnt, totalCh - firstCh);
1681 if (visibleCount <= 0)
1684 out.visibleCount = visibleCount;
1686 const int kTileMult = 5;
1687 int tilePixWidth = pw * kTileMult;
1688 float visibleSamples = pw * spp;
1691 out.sampleFirst = tileStart;
1693 QImage img(tilePixWidth, ph, QImage::Format_RGB32);
1694 img.fill(bgColor.rgb());
1697 p.setRenderHint(QPainter::Antialiasing,
false);
1699 float laneH =
static_cast<float>(ph) / visibleCount;
1700 int firstSample =
static_cast<int>(tileStart);
1701 int lastSample = firstSample +
static_cast<int>(tilePixWidth * spp) + 1;
1706 float samplesPerSec = sfreq;
1707 float firstBound = std::floor(
1708 (tileStart -
static_cast<float>(firstFileSample)) / samplesPerSec
1709 ) * samplesPerSec +
static_cast<float>(firstFileSample);
1712 long long bandIndex =
static_cast<long long>(
1713 (firstBound -
static_cast<float>(firstFileSample)) / samplesPerSec);
1714 bool oddBand = (bandIndex & 1) != 0;
1718 qBound(0, bgColor.red() - 10, 255),
1719 qBound(0, bgColor.green() - 10, 255),
1720 qBound(0, bgColor.blue() - 10, 255)
1723 for (
float s = firstBound; s < lastSample; s += samplesPerSec, oddBand = !oddBand) {
1726 float xStart = (s - tileStart) / spp;
1727 float xEnd = xStart + samplesPerSec / spp;
1728 xStart = qBound(0.f, xStart,
static_cast<float>(tilePixWidth));
1729 xEnd = qBound(0.f, xEnd,
static_cast<float>(tilePixWidth));
1731 p.fillRect(QRectF(xStart, 0, xEnd - xStart, ph), altColor);
1737 for (
int i = 0; i < visibleCount; ++i) {
1738 float yMid = (i + 0.5f) * laneH;
1739 float yTop = i * laneH;
1742 p.setPen(QPen(QColor(205, 205, 215), 1));
1743 p.drawLine(QPointF(0, yTop), QPointF(tilePixWidth, yTop));
1746 QPen guidePen(QColor(228, 228, 235), 1, Qt::DotLine);
1747 guidePen.setDashPattern({3, 4});
1749 p.drawLine(QPointF(0, yMid - laneH * 0.44f), QPointF(tilePixWidth, yMid - laneH * 0.44f));
1750 p.drawLine(QPointF(0, yMid + laneH * 0.44f), QPointF(tilePixWidth, yMid + laneH * 0.44f));
1752 p.setPen(QPen(QColor(210, 210, 218), 1));
1753 p.drawLine(QPointF(0, yMid), QPointF(tilePixWidth, yMid));
1757 static const float kNiceIntervals[] = {
1758 0.05f, 0.1f, 0.2f, 0.5f, 1.f, 2.f, 5.f, 10.f, 30.f, 60.f
1760 float pxPerSecond = sfreq / spp;
1761 float tickIntervalS = kNiceIntervals[0];
1762 for (
float iv : kNiceIntervals) {
1764 if (iv * pxPerSecond >= 80.f)
1767 float tickSamples = tickIntervalS * sfreq;
1768 float origin =
static_cast<float>(firstFileSample);
1769 float firstTick = std::ceil((tileStart - origin) / tickSamples) * tickSamples + origin;
1771 p.setPen(QPen(QColor(205, 205, 210), 1));
1772 for (
float s = firstTick; s < lastSample; s += tickSamples) {
1773 float xPx = (s - tileStart) / spp;
1774 p.drawLine(QPointF(xPx, 0), QPointF(xPx, ph));
1780 if (!annotations.isEmpty()) {
1781 QFont font = p.font();
1782 font.setPointSizeF(8.0);
1787 float xStart = (
static_cast<float>(annotation.startSample) - tileStart) / spp;
1788 float xEnd = (
static_cast<float>(annotation.endSample + 1) - tileStart) / spp;
1790 if (xEnd < -2.f || xStart > tilePixWidth + 2.f) {
1794 xStart = qBound(0.f, xStart,
static_cast<float>(tilePixWidth));
1795 xEnd = qBound(0.f, xEnd,
static_cast<float>(tilePixWidth));
1796 if (xEnd <= xStart) {
1800 QColor fillColor = annotation.color;
1801 fillColor.setAlpha(48);
1802 p.fillRect(QRectF(xStart, 0.f, xEnd - xStart,
static_cast<float>(ph)), fillColor);
1804 QColor borderColor = annotation.color;
1805 borderColor.setAlpha(165);
1806 p.setPen(QPen(borderColor, 1));
1807 p.drawLine(QPointF(xStart, 0.f), QPointF(xStart,
static_cast<float>(ph)));
1808 p.drawLine(QPointF(xEnd, 0.f), QPointF(xEnd,
static_cast<float>(ph)));
1810 if (!annotation.label.trimmed().isEmpty()) {
1811 const QString label = annotation.label.trimmed();
1812 QFontMetrics metrics(font);
1813 QRect labelRect = metrics.boundingRect(label);
1814 labelRect.adjust(-6, -2, 6, 2);
1815 const int labelX = qBound(4,
1816 static_cast<int>(xStart) + 4,
1817 qMax(4, tilePixWidth - labelRect.width() - 4));
1818 labelRect.moveTopLeft(QPoint(labelX, 4));
1819 QColor pillColor = annotation.color;
1820 pillColor.setAlpha(215);
1821 p.fillRect(labelRect, pillColor);
1822 p.setPen(Qt::white);
1823 p.drawText(labelRect, Qt::AlignCenter, label);
1829 for (
int i = 0; i < visibleCount; ++i) {
1830 int logIdx = firstCh + i;
1831 int ch = channelIndices.isEmpty() ? logIdx
1832 : (logIdx < channelIndices.size() ? channelIndices[logIdx] : -1);
1843 ch, firstSample, lastSample, tilePixWidth, vboFirst);
1844 if (verts.size() < 4)
1847 QColor col = info.
bad ? QColor(190, 40, 40) : info.color;
1848 QPen normalPen(col, 1.2);
1849 QPen clipPen(QColor(255, 0, 0), 1.6);
1851 float yMid = (i + 0.5f) * laneH;
1852 int nVerts = verts.size() / 2;
1856 float zMean = 0.f, zStd = 1.f;
1858 double sum = 0.0, sumSq = 0.0;
1859 for (
int v = 0; v < nVerts; ++v) {
1860 double a =
static_cast<double>(verts[v * 2 + 1]);
1864 zMean =
static_cast<float>(sum / nVerts);
1865 double var = sumSq / nVerts -
static_cast<double>(zMean) * zMean;
1866 zStd = var > 0.0 ?
static_cast<float>(qSqrt(var)) : 1.f;
1868 yScale = (laneH * 0.45f) / 4.f;
1875 bool doClip = showClipping && !info.
bad && !
zScoreMode && clipThresh > 0.f;
1879 p.setPen(normalPen);
1881 poly.reserve(nVerts);
1882 for (
int v = 0; v < nVerts; ++v) {
1883 float samplePos = vboFirst + verts[v * 2];
1884 float xPx = (samplePos - tileStart) / spp;
1885 float amp = verts[v * 2 + 1];
1887 float yPx = yMid - amp * yScale;
1888 poly.append(QPointF(xPx, yPx));
1890 p.drawPolyline(poly);
1894 seg.reserve(nVerts);
1895 bool prevClipped =
false;
1897 for (
int v = 0; v < nVerts; ++v) {
1898 float amp = verts[v * 2 + 1];
1899 float samplePos = vboFirst + verts[v * 2];
1900 float xPx = (samplePos - tileStart) / spp;
1901 float yPx = yMid - amp * yScale;
1902 bool clipped = qAbs(amp) >= clipThresh;
1904 if (v > 0 && clipped != prevClipped) {
1907 seg.append(QPointF(xPx, yPx));
1908 p.setPen(prevClipped ? clipPen : normalPen);
1909 p.drawPolyline(seg);
1913 seg.append(QPointF(xPx, yPx));
1914 prevClipped = clipped;
1917 if (seg.size() > 1) {
1918 p.setPen(prevClipped ? clipPen : normalPen);
1919 p.drawPolyline(seg);
1927 if (!events.isEmpty() && spp > 0.f) {
1929 float xF = (
static_cast<float>(ev.sample) - tileStart) / spp;
1930 if (xF < -2.f || xF > tilePixWidth + 2.f)
1932 int ix =
static_cast<int>(xF);
1934 QColor lineColor = ev.color;
1935 lineColor.setAlpha(180);
1936 p.setPen(QPen(lineColor, 1));
1937 p.drawLine(ix, 0, ix, ph);
1943 if (!epochMarkers.isEmpty() && spp > 0.f) {
1944 QPen epochPen(QColor(100, 100, 100, 140), 1, Qt::DashLine);
1946 for (
int trigSample : epochMarkers) {
1947 float xF = (
static_cast<float>(trigSample) - tileStart) / spp;
1948 if (xF < -2.f || xF > tilePixWidth + 2.f)
1950 int ix =
static_cast<int>(xF);
1951 p.drawLine(ix, 0, ix, ph);
1955 out.image = std::move(img);
1961void ChannelRhiView::drawOverlays()
1968 if (m_overlay && (m_crosshairEnabled || m_scalebarsVisible || m_rulerActive))
1969 m_overlay->update();
1974static QString formatAmplitude(
float amp,
const QString &unit)
1976 float absAmp = qAbs(amp);
1978 return QStringLiteral(
"0 ") + unit;
1980 return QString::number(amp * 1e12f,
'f', 1) + QStringLiteral(
" p") + unit;
1982 return QString::number(amp * 1e9f,
'f', 1) + QStringLiteral(
" n") + unit;
1984 return QString::number(amp * 1e6f,
'f', 1) + QStringLiteral(
" µ") + unit;
1986 return QString::number(amp * 1e3f,
'f', 1) + QStringLiteral(
" m") + unit;
1987 return QString::number(amp,
'f', 3) + QStringLiteral(
" ") + unit;
1990static QString unitForType(
const QString &typeLabel)
1992 if (typeLabel == QStringLiteral(
"MEG grad"))
1993 return QStringLiteral(
"T/m");
1994 if (typeLabel == QStringLiteral(
"MEG mag") || typeLabel == QStringLiteral(
"MEG"))
1995 return QStringLiteral(
"T");
1996 if (typeLabel == QStringLiteral(
"EEG") ||
1997 typeLabel == QStringLiteral(
"EOG") ||
1998 typeLabel == QStringLiteral(
"ECG") ||
1999 typeLabel == QStringLiteral(
"EMG"))
2000 return QStringLiteral(
"V");
2001 return QStringLiteral(
"AU");
2008 if (m_crosshairX < 0 || m_crosshairY < 0)
2013 const int w = width();
2014 const int h = height();
2017 QPen crossPen(QColor(80, 80, 80, 160), 1, Qt::DashLine);
2019 p.drawLine(m_crosshairX, 0, m_crosshairX, h);
2020 p.drawLine(0, m_crosshairY, w, m_crosshairY);
2022 int sample =
static_cast<int>(m_scrollSample +
static_cast<float>(m_crosshairX) * m_samplesPerPixel);
2023 float timeSec = (m_sfreq > 0.f) ?
static_cast<float>(sample - m_firstFileSample) / m_sfreq : 0.f;
2024 QString channelLabel;
2028 if (m_butterflyMode) {
2030 const auto groups = butterflyTypeGroups();
2031 int nLanes = groups.size();
2034 float laneH =
static_cast<float>(h) / nLanes;
2035 int lane = qBound(0,
static_cast<int>(m_crosshairY / laneH), nLanes - 1);
2036 channelLabel = groups[lane].typeLabel;
2037 unitStr = unitForType(groups[lane].typeLabel);
2041 int visCnt = qMin(m_visibleChannelCount, totalCh - m_firstVisibleChannel);
2045 float laneH =
static_cast<float>(h) / visCnt;
2046 int row = qBound(0,
static_cast<int>(m_crosshairY / laneH), visCnt - 1);
2047 int ch = actualChannelAt(m_firstVisibleChannel + row);
2051 auto info = m_model->channelInfo(ch);
2052 value = m_model->sampleValueAt(ch, sample);
2053 channelLabel = info.
name;
2059 if (m_useClockTime && timeSec >= 0.f) {
2060 int totalMs =
static_cast<int>(timeSec * 1000.f + 0.5f);
2061 int m = totalMs / 60000;
2062 int sec = (totalMs % 60000) / 1000;
2063 int ms = totalMs % 1000;
2064 timeStr = QString(
"%1:%2.%3")
2065 .arg(m, 2, 10, QChar(
'0'))
2066 .arg(sec, 2, 10, QChar(
'0'))
2067 .arg(ms, 3, 10, QChar(
'0'));
2069 timeStr = QString::number(
static_cast<double>(timeSec),
'f', 3) + QStringLiteral(
" s");
2071 QString label = QString(
"%1 %2 %3")
2074 formatAmplitude(value, unitStr));
2077 f.setPointSizeF(8.5);
2080 QRect labelRect = fm.boundingRect(label);
2081 int lx = m_crosshairX + 10;
2082 int ly = m_crosshairY - 10;
2083 if (lx + labelRect.width() + 8 > w)
2084 lx = m_crosshairX - labelRect.width() - 18;
2085 if (ly - labelRect.height() < 4)
2086 ly = m_crosshairY + labelRect.height() + 6;
2087 labelRect.moveTopLeft(QPoint(lx, ly - labelRect.height()));
2088 labelRect.adjust(-4, -2, 4, 2);
2089 p.fillRect(labelRect, QColor(255, 255, 255, 220));
2090 p.setPen(QColor(30, 30, 30));
2091 p.drawText(labelRect, Qt::AlignCenter, label);
2098 if (m_crosshairX < 0 || m_crosshairY < 0)
2103 const int h = height();
2104 int sample =
static_cast<int>(m_scrollSample +
static_cast<float>(m_crosshairX) * m_samplesPerPixel);
2105 float timeSec = (m_sfreq > 0.f) ?
static_cast<float>(sample - m_firstFileSample) / m_sfreq : 0.f;
2107 if (m_butterflyMode) {
2108 const auto groups = butterflyTypeGroups();
2109 int nLanes = groups.size();
2110 if (nLanes <= 0)
return;
2111 float laneH =
static_cast<float>(h) / nLanes;
2112 int lane = qBound(0,
static_cast<int>(m_crosshairY / laneH), nLanes - 1);
2114 groups[lane].typeLabel,
2115 unitForType(groups[lane].typeLabel));
2118 int visCnt = qMin(m_visibleChannelCount, totalCh - m_firstVisibleChannel);
2119 if (visCnt <= 0)
return;
2120 float laneH =
static_cast<float>(h) / visCnt;
2121 int row = qBound(0,
static_cast<int>(m_crosshairY / laneH), visCnt - 1);
2122 int ch = actualChannelAt(m_firstVisibleChannel + row);
2124 auto info = m_model->channelInfo(ch);
2125 float value = m_model->sampleValueAt(ch, sample);
2138 if (m_butterflyMode) {
2139 visCnt = butterflyLaneCount();
2142 visCnt = qMin(m_visibleChannelCount, totalCh - m_firstVisibleChannel);
2147 float laneH =
static_cast<float>(height()) / visCnt;
2150 QMap<QString, float> typeScales;
2151 if (m_butterflyMode) {
2152 const auto groups = butterflyTypeGroups();
2153 for (
const auto &g : groups)
2154 if (g.amplitudeMax > 0.f)
2155 typeScales[g.typeLabel] = g.amplitudeMax;
2157 for (
int i = 0; i < visCnt; ++i) {
2158 int ch = actualChannelAt(m_firstVisibleChannel + i);
2159 if (ch < 0)
continue;
2160 auto info = m_model->channelInfo(ch);
2166 if (typeScales.isEmpty())
2170 f.setPointSizeF(8.0);
2175 const int margin = 12;
2176 const int barHeight = qBound(20,
static_cast<int>(laneH * 0.35f), 60);
2177 int x = width() - margin;
2178 int y = height() - margin;
2180 for (
auto it = typeScales.constEnd(); it != typeScales.constBegin(); ) {
2182 QString unit = unitForType(it.key());
2183 float ampValue = it.value();
2184 QString label = it.key() + QStringLiteral(
": ") + formatAmplitude(ampValue, unit);
2186 int textW = fm.horizontalAdvance(label);
2187 int barX = x - textW - 14;
2190 QRect bgRect(barX - 4, y - barHeight - fm.height() - 4, textW + 22, barHeight + fm.height() + 8);
2191 p.fillRect(bgRect, QColor(255, 255, 255, 200));
2194 QPen barPen(QColor(40, 40, 40), 2);
2196 int barTop = y - barHeight;
2197 p.drawLine(barX + 4, barTop, barX + 4, y);
2199 p.drawLine(barX, barTop, barX + 8, barTop);
2200 p.drawLine(barX, y, barX + 8, y);
2203 p.setPen(QColor(30, 30, 30));
2204 p.drawText(barX + 14, y - barHeight / 2 + fm.ascent() / 2, label);
2206 y -= barHeight + fm.height() + 16;
2214 int x0 = m_rulerX0, y0 = m_rulerY0;
2215 int x1 = m_rulerX1, y1 = m_rulerY1;
2217 const bool snapH = (m_rulerSnap == RulerSnap::Horizontal);
2218 const bool snapV = (m_rulerSnap == RulerSnap::Vertical);
2220 const QColor activeColor(40, 120, 200, 220);
2221 const QColor dimColor(130, 160, 200, 120);
2228 measured = QRect(QPoint(qMin(x0, x1), 0),
2229 QPoint(qMax(x0, x1), height()));
2231 measured = QRect(QPoint(0, qMin(y0, y1)),
2232 QPoint(width(), qMax(y0, y1)));
2234 measured = QRect(QPoint(qMin(x0, x1), qMin(y0, y1)),
2235 QPoint(qMax(x0, x1), qMax(y0, y1)));
2236 p.fillRect(measured, QColor(255, 255, 255, 60));
2238 p.setPen(QPen(QColor(40, 120, 200, 80), 1));
2239 p.drawRect(measured);
2243 QPen vLinePen(snapV ? dimColor : activeColor, 1, Qt::DashLine);
2245 p.drawLine(x0, 0, x0, height());
2247 p.drawLine(x1, 0, x1, height());
2251 QPen hGuidePen(activeColor, 1, Qt::DashLine);
2252 p.setPen(hGuidePen);
2253 p.drawLine(0, y0, width(), y0);
2254 p.drawLine(0, y1, width(), y1);
2258 QPen hLinePen(snapV ? dimColor : activeColor, snapH ? 2 : 1);
2261 p.drawLine(qMin(x0, x1), y0, qMax(x0, x1), y0);
2264 QPen vSpanPen(snapH ? dimColor : activeColor, snapV ? 2 : 1);
2267 p.drawLine(x0, qMin(y0, y1), x0, qMax(y0, y1));
2271 p.setPen(QPen(activeColor, 1));
2272 p.drawLine(x0 - tickLen, y0, x0 + tickLen, y0);
2273 p.drawLine(x1 - tickLen, y0, x1 + tickLen, y0);
2276 p.setPen(QPen(activeColor, 1));
2277 p.drawLine(x0, y0 - tickLen, x0, y0 + tickLen);
2278 p.drawLine(x0, y1 - tickLen, x0, y1 + tickLen);
2282 float deltaSamples =
static_cast<float>(x1 - x0) * m_samplesPerPixel;
2283 float deltaSec = (m_sfreq > 0.f) ? deltaSamples / m_sfreq : 0.f;
2285 float deltaAmp = 0.f;
2286 QString ampUnit = QStringLiteral(
"AU");
2289 int visCnt = qMin(m_visibleChannelCount, totalCh - m_firstVisibleChannel);
2291 float laneH =
static_cast<float>(height()) / visCnt;
2292 int row = qBound(0,
static_cast<int>(y0 / laneH), visCnt - 1);
2293 int ch = actualChannelAt(m_firstVisibleChannel + row);
2295 auto info = m_model->channelInfo(ch);
2297 float dyPx =
static_cast<float>(y1 - y0);
2299 deltaAmp = -dyPx * yScale;
2306 auto fmtTime = [](
float sec) -> QString {
2307 float absSec = qAbs(sec);
2309 return QString::number(sec * 1000.f,
'f', 1) + QStringLiteral(
" ms");
2310 return QString::number(sec,
'f', 3) + QStringLiteral(
" s");
2312 auto fmtAmp = [](
float amp,
const QString &unit) -> QString {
2313 float absAmp = qAbs(amp);
2315 return QString::number(amp * 1e9f,
'f', 3) + QStringLiteral(
" n") + unit;
2317 return QString::number(amp * 1e6f,
'f', 3) + QStringLiteral(
" µ") + unit;
2319 return QString::number(amp * 1e3f,
'f', 3) + QStringLiteral(
" m") + unit;
2320 return QString::number(amp,
'f', 3) + QStringLiteral(
" ") + unit;
2323 QString timeLabel = QStringLiteral(
"\u0394T = ") + fmtTime(deltaSec);
2324 QString ampLabel = QStringLiteral(
"\u0394A = ") + fmtAmp(deltaAmp, ampUnit);
2327 f.setPointSizeF(9.0);
2334 int labelX = (x0 + x1) / 2;
2335 int labelY = y0 - 6;
2339 QRect tRect = fm.boundingRect(timeLabel);
2340 tRect.moveCenter(QPoint(labelX, labelY));
2341 tRect.adjust(-4, -2, 4, 2);
2342 p.fillRect(tRect, QColor(255, 255, 255, 210));
2343 p.setPen(QColor(20, 80, 160));
2344 p.drawText(tRect, Qt::AlignCenter, timeLabel);
2349 int aLabelX = x0 + 8;
2350 int aLabelY = (y0 + y1) / 2;
2351 QRect aRect = fm.boundingRect(ampLabel);
2352 aRect.moveCenter(QPoint(aLabelX + aRect.width() / 2, aLabelY));
2353 aRect.adjust(-4, -2, 4, 2);
2354 p.fillRect(aRect, QColor(255, 255, 255, 210));
2355 p.setPen(QColor(20, 80, 160));
2356 p.drawText(aRect, Qt::AlignCenter, ampLabel);
2364 const int x0 = qMin(m_annSelX0, m_annSelX1);
2365 const int x1 = qMax(m_annSelX0, m_annSelX1);
2366 const int h = height();
2369 p.fillRect(QRect(x0, 0, x1 - x0, h), QColor(210, 60, 60, 50));
2372 QPen borderPen(QColor(210, 60, 60, 180), 2);
2373 p.setPen(borderPen);
2374 p.drawLine(x0, 0, x0, h);
2375 p.drawLine(x1, 0, x1, h);
2378 if (m_sfreq > 0.f && m_samplesPerPixel > 0.f) {
2379 float deltaSamples =
static_cast<float>(x1 - x0) * m_samplesPerPixel;
2380 float deltaSec = deltaSamples / m_sfreq;
2383 label = QString::number(deltaSec * 1000.f,
'f', 0) + QStringLiteral(
" ms");
2385 label = QString::number(deltaSec,
'f', 2) + QStringLiteral(
" s");
2388 f.setPointSizeF(8.0);
2392 QRect labelRect = fm.boundingRect(label);
2393 labelRect.adjust(-6, -2, 6, 2);
2394 labelRect.moveTopLeft(QPoint(x0 + 4, 4));
2395 p.fillRect(labelRect, QColor(210, 60, 60, 215));
2396 p.setPen(Qt::white);
2397 p.drawText(labelRect, Qt::AlignCenter, label);
2407 QRhiWidget::resizeEvent(event);
2409 m_overlayDirty =
true;
2411 if (m_overlay) m_overlay->syncSize();
2420 const QPoint delta =
event->angleDelta();
2422 if (event->modifiers() & Qt::ControlModifier) {
2424 float factor = (delta.y() > 0) ? 0.8f : 1.25f;
2425 zoomTo(m_samplesPerPixel * factor, 150);
2427 }
else if (qAbs(delta.x()) > qAbs(delta.y())) {
2430 float step = width() * m_samplesPerPixel * 0.1f * m_scrollSpeedFactor
2431 * (delta.x() > 0 ? -1.f : 1.f);
2432 scrollTo(m_scrollSample + step, 100);
2435 }
else if (m_wheelScrollsChannels) {
2437 int channelStep = (delta.y() > 0) ? -1 : 1;
2443 float step = width() * m_samplesPerPixel * 0.15f * m_scrollSpeedFactor
2444 * (delta.y() > 0 ? -1.f : 1.f);
2445 scrollTo(m_scrollSample + step, 100);
2458 if (event->button() == Qt::RightButton) {
2460 if (m_pInertialAnim) {
2461 m_pInertialAnim->stop();
2462 m_pInertialAnim =
nullptr;
2465 if (m_annotationSelectionEnabled) {
2467 m_annSelecting =
true;
2468 m_annSelX0 = m_annSelX1 =
event->position().toPoint().x();
2469 if (m_overlay) m_overlay->repaint();
2472 m_rulerActive =
true;
2473 m_rulerSnap = RulerSnap::Free;
2474 m_rulerX0 = m_rulerX1 = m_rulerRawX1 =
event->position().toPoint().x();
2475 m_rulerY0 = m_rulerY1 = m_rulerRawY1 =
event->position().toPoint().y();
2476 if (m_overlay) m_overlay->repaint();
2483 (event->button() == Qt::MiddleButton ||
2484 (event->button() == Qt::LeftButton && (event->modifiers() & Qt::AltModifier)))) {
2486 m_dragStartX =
event->position().toPoint().x();
2487 m_dragStartScroll = m_scrollSample;
2491 if (event->button() == Qt::LeftButton) {
2493 if (m_pInertialAnim) {
2494 m_pInertialAnim->stop();
2495 m_pInertialAnim =
nullptr;
2499 if (m_annotationSelectionEnabled && !m_annotations.isEmpty()) {
2500 bool isStart =
false;
2501 int hitIdx = hitTestAnnotationBoundary(event->position().toPoint().x(), isStart);
2503 m_annDragging =
true;
2504 m_annDragIndex = hitIdx;
2505 m_annDragIsStart = isStart;
2513 float samplePos = m_scrollSample
2514 +
static_cast<float>(
event->position().x()) * m_samplesPerPixel;
2520 m_leftButtonDown =
true;
2521 m_leftDragActivated =
false;
2522 m_leftDownX =
event->position().toPoint().x();
2523 m_leftDownScroll = m_scrollSample;
2524 m_velocityHistory.clear();
2525 m_dragTimer.start();
2526 m_velocityHistory.append({m_leftDownX, 0});
2530 QRhiWidget::mousePressEvent(event);
2538 if (m_annDragging) {
2540 int newSample =
static_cast<int>(m_scrollSample
2541 +
static_cast<float>(
event->position().toPoint().x()) * m_samplesPerPixel);
2542 newSample = qMax(newSample, m_firstFileSample);
2543 if (m_lastFileSample >= 0)
2544 newSample = qMin(newSample, m_lastFileSample);
2546 if (m_annDragIndex >= 0 && m_annDragIndex < m_annotations.size()) {
2547 if (m_annDragIsStart)
2548 m_annotations[m_annDragIndex].startSample = newSample;
2550 m_annotations[m_annDragIndex].endSample = newSample;
2551 m_overlayDirty =
true;
2560 if (m_annSelecting) {
2561 m_annSelX1 =
event->position().toPoint().x();
2562 if (m_overlay) m_overlay->repaint();
2567 if (m_rulerActive) {
2568 m_rulerRawX1 =
event->position().toPoint().x();
2569 m_rulerRawY1 =
event->position().toPoint().y();
2573 int dx = qAbs(m_rulerRawX1 - m_rulerX0);
2574 int dy = qAbs(m_rulerRawY1 - m_rulerY0);
2575 const int kSnapThresh = 8;
2576 if (dx < kSnapThresh && dy < kSnapThresh) {
2577 m_rulerSnap = RulerSnap::Free;
2578 }
else if (dx > dy * 2) {
2579 m_rulerSnap = RulerSnap::Horizontal;
2580 }
else if (dy > dx * 2) {
2581 m_rulerSnap = RulerSnap::Vertical;
2583 m_rulerSnap = RulerSnap::Free;
2587 switch (m_rulerSnap) {
2588 case RulerSnap::Horizontal:
2589 m_rulerX1 = m_rulerRawX1;
2590 m_rulerY1 = m_rulerY0;
2592 case RulerSnap::Vertical:
2593 m_rulerX1 = m_rulerX0;
2594 m_rulerY1 = m_rulerRawY1;
2597 m_rulerX1 = m_rulerRawX1;
2598 m_rulerY1 = m_rulerRawY1;
2602 if (m_overlay) m_overlay->repaint();
2608 int dx =
event->position().toPoint().x() - m_dragStartX;
2609 float newScroll = m_dragStartScroll -
static_cast<float>(dx) * m_samplesPerPixel;
2614 if (m_leftButtonDown) {
2615 int x =
event->position().toPoint().x();
2616 int dx = x - m_leftDownX;
2617 if (!m_leftDragActivated && qAbs(dx) > 5)
2618 m_leftDragActivated =
true;
2619 if (m_leftDragActivated) {
2620 float newScroll = m_leftDownScroll -
static_cast<float>(dx) * m_samplesPerPixel;
2624 qint64 now = m_dragTimer.elapsed();
2625 m_velocityHistory.append({x, now});
2626 while (m_velocityHistory.size() > 1 &&
2627 now - m_velocityHistory.first().t > 100)
2628 m_velocityHistory.removeFirst();
2636 if (m_crosshairEnabled) {
2637 m_crosshairX =
event->position().toPoint().x();
2638 m_crosshairY =
event->position().toPoint().y();
2639 if (m_overlay) m_overlay->repaint();
2647 if (m_annotationSelectionEnabled && !m_annotations.isEmpty()) {
2648 bool isStart =
false;
2649 int hitIdx = hitTestAnnotationBoundary(event->position().toPoint().x(), isStart);
2651 if (m_annHoverIndex != hitIdx || m_annHoverIsStart != isStart) {
2652 m_annHoverIndex = hitIdx;
2653 m_annHoverIsStart = isStart;
2654 setCursor(Qt::SizeHorCursor);
2656 }
else if (m_annHoverIndex >= 0) {
2657 m_annHoverIndex = -1;
2662 QRhiWidget::mouseMoveEvent(event);
2670 if (m_annDragging && event->button() == Qt::LeftButton) {
2671 int newSample =
static_cast<int>(m_scrollSample
2672 +
static_cast<float>(
event->position().toPoint().x()) * m_samplesPerPixel);
2673 newSample = qMax(newSample, m_firstFileSample);
2674 if (m_lastFileSample >= 0)
2675 newSample = qMin(newSample, m_lastFileSample);
2678 m_annDragging =
false;
2679 m_annDragIndex = -1;
2685 if (m_annSelecting && event->button() == Qt::RightButton) {
2686 m_annSelX1 =
event->position().toPoint().x();
2687 m_annSelecting =
false;
2688 if (m_overlay) m_overlay->repaint();
2690 const int x0 = qMin(m_annSelX0, m_annSelX1);
2691 const int x1 = qMax(m_annSelX0, m_annSelX1);
2692 if (qAbs(x1 - x0) > 3) {
2693 int startSample =
static_cast<int>(m_scrollSample +
static_cast<float>(x0) * m_samplesPerPixel);
2694 int endSample =
static_cast<int>(m_scrollSample +
static_cast<float>(x1) * m_samplesPerPixel);
2696 startSample = qMax(startSample, m_firstFileSample);
2697 if (m_lastFileSample >= 0)
2698 endSample = qMin(endSample, m_lastFileSample);
2700 if (endSample >= startSample)
2707 if (m_rulerActive && event->button() == Qt::RightButton) {
2708 m_rulerRawX1 =
event->position().toPoint().x();
2709 m_rulerRawY1 =
event->position().toPoint().y();
2711 switch (m_rulerSnap) {
2712 case RulerSnap::Horizontal:
2713 m_rulerX1 = m_rulerRawX1; m_rulerY1 = m_rulerY0;
break;
2714 case RulerSnap::Vertical:
2715 m_rulerX1 = m_rulerX0; m_rulerY1 = m_rulerRawY1;
break;
2717 m_rulerX1 = m_rulerRawX1; m_rulerY1 = m_rulerRawY1;
break;
2719 m_rulerActive =
false;
2720 if (m_overlay) m_overlay->repaint();
2726 if (m_dragging && (event->button() == Qt::MiddleButton ||
2727 event->button() == Qt::LeftButton)) {
2732 if (event->button() == Qt::LeftButton && m_leftButtonDown) {
2733 if (!m_leftDragActivated) {
2735 float samplePos = m_leftDownScroll
2736 +
static_cast<float>(
event->position().x()) * m_samplesPerPixel;
2740 if (m_velocityHistory.size() >= 2) {
2741 auto oldest = m_velocityHistory.first();
2742 auto newest = m_velocityHistory.last();
2743 float dt =
static_cast<float>(newest.t - oldest.t);
2745 float dx =
static_cast<float>(newest.x - oldest.x);
2747 float velSampPerMs = -(dx / dt) * m_samplesPerPixel;
2748 float speed = qAbs(velSampPerMs);
2752 float durationMs = qBound(500.f, speed * 1.0f, 5000.f);
2753 float targetScroll = m_scrollSample + velSampPerMs * durationMs / 3.f;
2754 targetScroll = qMax(targetScroll,
static_cast<float>(m_firstFileSample));
2756 m_pInertialAnim =
new QPropertyAnimation(
this,
"scrollSample",
this);
2757 m_pInertialAnim->setDuration(
static_cast<int>(durationMs));
2758 m_pInertialAnim->setEasingCurve(QEasingCurve::OutCubic);
2759 m_pInertialAnim->setStartValue(m_scrollSample);
2760 m_pInertialAnim->setEndValue(targetScroll);
2761 connect(m_pInertialAnim, &QPropertyAnimation::finished,
this, [
this]() {
2762 m_pInertialAnim =
nullptr;
2764 m_pInertialAnim->start(QAbstractAnimation::DeleteWhenStopped);
2769 m_leftButtonDown =
false;
2770 m_leftDragActivated =
false;
2774 QRhiWidget::mouseReleaseEvent(event);
2781 if (!m_model || event->button() != Qt::LeftButton) {
2782 QRhiWidget::mouseDoubleClickEvent(event);
2787 int visCnt = qMin(m_visibleChannelCount, totalCh - m_firstVisibleChannel);
2791 float laneH =
static_cast<float>(height()) / visCnt;
2792 int row =
static_cast<int>(
event->position().y() / laneH);
2793 if (row >= 0 && row < visCnt) {
2794 int ch = actualChannelAt(m_firstVisibleChannel + row);
2796 auto info = m_model->channelInfo(ch);
2797 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).
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)
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.