v2.0.0
Loading...
Searching...
No Matches
brainview.cpp
Go to the documentation of this file.
1//=============================================================================================================
12
13//=============================================================================================================
14// INCLUDES
15//=============================================================================================================
16
17
18#include "brainview.h"
19#include "brainrenderer.h"
26#include "core/surfacekeys.h"
27#include "core/dataloader.h"
28#include "input/raypicker.h"
32
33#include <rhi/qrhi.h>
39
40#include <Eigen/Dense>
41#include <QMatrix4x4>
42#include <QDebug>
43#include <QLabel>
44#include <QFrame>
45#include <QMouseEvent>
46#include <QKeyEvent>
47#include <QWheelEvent>
48#include <QResizeEvent>
49#include <QSettings>
50#include <QTimer>
51#include <QMenu>
52#include <QStandardItem>
53#include <QCoreApplication>
54#include <QElapsedTimer>
55#include <QDateTime>
56#include <QDir>
57#include <QFileInfo>
58#include <algorithm>
59#include <cmath>
60
61#include <mne/mne_bem.h>
64#include <fiff/fiff_constants.h>
68
69using namespace FIFFLIB;
70
71namespace DISP3DLIB
72{
73
74namespace
75{
76
77QColor itemColor(const AbstractTreeItem& item)
78{
79 QColor color = item.color();
80 color.setAlphaF(color.alphaF() * item.alpha());
81 return color;
82}
83
84} // namespace
85
86// QSettings is constructed with its default ctor below; it picks up the
87// organisation and application names that each host (mne_align,
88// mne_inspect, ex_disp_3D, ...) sets on QCoreApplication in its main(),
89// so persisted view state is naturally scoped per-application.
90
91//=============================================================================================================
92// DEFINE MEMBER METHODS
93//=============================================================================================================
94
95//=============================================================================================================
96
97BrainView::BrainView(QWidget* parent)
98: QRhiWidget(parent)
99{
100 setMinimumSize(800, 600);
101 setSampleCount(1);
102 setAutoRenderTarget(false); // We manage our own dual render targets
103
104#if defined(WASMBUILD) || defined(__EMSCRIPTEN__)
105 setApi(Api::OpenGL); // WebGL 2 (OpenGL ES 3.0) on WASM
106#elif defined(Q_OS_MACOS) || defined(Q_OS_IOS)
107 setApi(Api::Metal);
108#elif defined(Q_OS_WIN)
109 setApi(Api::Direct3D11);
110#else
111 setApi(Api::OpenGL);
112#endif
113
114 setMouseTracking(true); // Enable hover events
115
116 // No periodic update timer — redraws are demand-driven via update().
117 // Every setter that changes scene state calls m_sceneDirty = true
118 // followed by update(), which coalesces into one render() per frame.
119
120 m_fpsLabel = new QLabel(this);
121 m_fpsLabel->setStyleSheet("color: white; font-weight: bold; font-family: monospace; font-size: 13px; background: transparent; padding: 5px;");
122 m_fpsLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
123 m_fpsLabel->setAlignment(Qt::AlignRight | Qt::AlignTop);
124 m_fpsLabel->setText("FPS: --.-\nVertices: 0");
125 m_fpsLabel->adjustSize();
126 m_fpsLabel->move(width() - m_fpsLabel->width() - 10, 10);
127 m_fpsLabel->raise();
128
129 m_singleViewInfoLabel = new QLabel(this);
130 m_singleViewInfoLabel->setStyleSheet("color: white; font-family: monospace; font-size: 10px; background: rgba(0,0,0,110); border-radius: 3px; padding: 2px 4px;");
131 m_singleViewInfoLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
132 m_singleViewInfoLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop);
133 m_singleViewInfoLabel->setText("");
134 m_singleViewInfoLabel->adjustSize();
135 m_singleViewInfoLabel->hide();
136
137 m_fpsTimer.start();
138
139 m_regionLabel = new QLabel(this);
140 m_regionLabel->setStyleSheet("color: white; font-weight: bold; font-family: sans-serif; font-size: 16px; background: transparent; padding: 5px;");
141 m_regionLabel->setText("");
142 m_regionLabel->move(10, 10);
143 m_regionLabel->resize(300, 30);
144 m_regionLabel->hide();
145
146 // ── Initialise viewport labels (sized to kDefaultViewportCount) ────────
147 m_subViews.resize(kDefaultViewportCount);
148 m_viewportNameLabels.resize(kDefaultViewportCount, nullptr);
149 m_viewportInfoLabels.resize(kDefaultViewportCount, nullptr);
150 for (int i = 0; i < kDefaultViewportCount; ++i) {
151 m_subViews[i] = SubView::defaultForIndex(i);
152
153 m_viewportNameLabels[i] = new QLabel(this);
154 m_viewportNameLabels[i]->setStyleSheet("color: white; font-weight: bold; font-family: sans-serif; font-size: 12px; background: transparent; padding: 2px 4px;");
155 m_viewportNameLabels[i]->setAttribute(Qt::WA_TransparentForMouseEvents);
156 m_viewportNameLabels[i]->setText(multiViewPresetName(m_subViews[i].preset));
157 m_viewportNameLabels[i]->adjustSize();
158 m_viewportNameLabels[i]->hide();
159
160 m_viewportInfoLabels[i] = new QLabel(this);
161 m_viewportInfoLabels[i]->setStyleSheet("color: white; font-family: monospace; font-size: 10px; background: rgba(0,0,0,110); border-radius: 3px; padding: 2px 4px;");
162 m_viewportInfoLabels[i]->setAttribute(Qt::WA_TransparentForMouseEvents);
163 m_viewportInfoLabels[i]->setAlignment(Qt::AlignLeft | Qt::AlignTop);
164 m_viewportInfoLabels[i]->setText("");
165 m_viewportInfoLabels[i]->adjustSize();
166 m_viewportInfoLabels[i]->hide();
167 }
168
169 m_verticalSeparator = new QFrame(this);
170 m_verticalSeparator->setFrameShape(QFrame::NoFrame);
171 m_verticalSeparator->setAttribute(Qt::WA_TransparentForMouseEvents);
172 m_verticalSeparator->hide();
173
174 m_horizontalSeparator = new QFrame(this);
175 m_horizontalSeparator->setFrameShape(QFrame::NoFrame);
176 m_horizontalSeparator->setAttribute(Qt::WA_TransparentForMouseEvents);
177 m_horizontalSeparator->hide();
178
179 QColor sepColor = palette().color(QPalette::Midlight);
180 if (sepColor.alpha() == 255) {
181 sepColor.setAlpha(180);
182 }
183 const QString sepStyle = QString("background-color: rgba(%1,%2,%3,%4);")
184 .arg(sepColor.red())
185 .arg(sepColor.green())
186 .arg(sepColor.blue())
187 .arg(sepColor.alpha());
188 m_verticalSeparator->setStyleSheet(sepStyle);
189 m_horizontalSeparator->setStyleSheet(sepStyle);
190
191 loadMultiViewSettings();
192 updateViewportSeparators();
193 updateOverlayLayout();
194
195 // Setup Debug Pointer: Semi-transparent sphere for subtle intersection indicator
196 m_debugPointerSurface = MeshFactory::createSphere(QVector3D(0, 0, 0), 0.002f,
197 QColor(200, 255, 255, 160));
198
199 // ── Connect SourceEstimateManager signals ─────────────────────────
200 connect(&m_sourceManager, &SourceEstimateManager::loaded,
201 this, &BrainView::onSourceEstimateLoaded);
202 connect(&m_sourceManager, &SourceEstimateManager::thresholdsUpdated,
204 connect(&m_sourceManager, &SourceEstimateManager::timePointChanged,
206 connect(&m_sourceManager, &SourceEstimateManager::loadingProgress,
208 connect(&m_sourceManager, &SourceEstimateManager::realtimeColorsAvailable,
209 this, &BrainView::onRealtimeColorsAvailable);
210
211 // RtSensorStreamManager → BrainView
212 connect(&m_sensorStreamManager, &RtSensorStreamManager::colorsAvailable,
213 this, &BrainView::onSensorStreamColorsAvailable);
214
215 // Video overlay starts disabled with a default focus point near the
216 // top of a typical FreeSurfer head; the application toggles it on.
217 m_videoOverlay = std::make_unique<DISP3DLIB::VideoOverlay>();
218}
219
220//=============================================================================================================
221
223{
224 saveMultiViewSettings();
225}
226
227//=============================================================================================================
228
230{
231 if (model == m_model)
232 return;
233
234 if (m_model) {
235 disconnect(m_model, nullptr, this, nullptr);
236 for (auto it = m_itemSurfaceMap.cbegin(); it != m_itemSurfaceMap.cend(); ++it) {
237 for (auto sit = m_surfaces.begin(); sit != m_surfaces.end();) {
238 sit = (sit.value() == it.value()) ? m_surfaces.erase(sit) : std::next(sit);
239 }
240 }
241 m_itemSurfaceMap.clear();
242 m_itemDipoleMap.clear();
243 m_hoveredItem = nullptr;
244 m_activeSurface.reset();
245 }
246
247 m_model = model;
248 if (!m_model) {
249 updateSceneBounds();
250 m_sceneDirty = true;
251 update();
252 return;
253 }
254 connect(m_model, &BrainTreeModel::rowsInserted, this, &BrainView::onRowsInserted);
255 connect(m_model, &BrainTreeModel::dataChanged, this, &BrainView::onDataChanged);
256
257 if (m_model->rowCount() > 0) {
258 onRowsInserted(QModelIndex(), 0, m_model->rowCount() - 1);
259 } else {
260 updateSceneBounds();
261 m_sceneDirty = true;
262 update();
263 }
264}
265
266//=============================================================================================================
267
268void BrainView::setInitialCameraRotation(const QQuaternion& rotation)
269{
270 m_cameraRotation = rotation;
271 saveMultiViewSettings();
272 m_sceneDirty = true;
273 update();
274}
275
276void BrainView::onRowsInserted(const QModelIndex& parent, int first, int last)
277{
278 if (!m_model)
279 return;
280
281 for (int i = first; i <= last; ++i) {
282 QModelIndex index = m_model->index(i, 0, parent);
283 QStandardItem* item = m_model->itemFromIndex(index);
284
285 AbstractTreeItem* absItem = dynamic_cast<AbstractTreeItem*>(item);
286
287 // Handle FsSurface Items
289 SurfaceTreeItem* surfItem = static_cast<SurfaceTreeItem*>(absItem);
290 auto brainSurf = std::make_shared<BrainSurface>();
291
292 // Load geometry from item
293 brainSurf->fromSurface(surfItem->surfaceData());
294
295 // Determine Hemisphere from Parent
296 if (absItem->parent()) {
297 QString parentText = absItem->parent()->text();
298 if (parentText == "lh")
299 brainSurf->setHemi(0);
300 else if (parentText == "rh")
301 brainSurf->setHemi(1);
302 }
303
304 // Set properties
305 brainSurf->setVisible(surfItem->isVisible());
306
307 // Brain surfaces (pial, white, inflated, etc.) are brain tissue
308 brainSurf->setTissueType(BrainSurface::TissueBrain);
309
310 m_itemSurfaceMap[item] = brainSurf;
311
312 // Key generation: "hemi_type" e.g. "lh_pial"
313 QString key;
314 if (absItem->parent()) {
315 key = absItem->parent()->text() + "_" + surfItem->text();
316 } else {
317 key = surfItem->text();
318 }
319 m_surfaces[key] = brainSurf;
320
321 // Check for annotations
322 if (!surfItem->annotationData().isEmpty()) {
323 brainSurf->addAnnotation(surfItem->annotationData());
324 }
325
326 // Set active if first
327 if (!m_activeSurface) {
328 m_activeSurface = brainSurf;
329 m_activeSurfaceType = surfItem->text();
330 }
331 }
332 // Check for BEM Item (using dynamic_cast for safety)
333 BemTreeItem* bemItem = dynamic_cast<BemTreeItem*>(absItem);
334 if (bemItem) {
335 const MNELIB::MNEBemSurface& bemSurfData = bemItem->bemSurfaceData();
336
337 auto brainSurf = std::make_shared<BrainSurface>();
338
339 // Load BEM geometry with color from item
340 brainSurf->fromBemSurface(bemSurfData, itemColor(*bemItem));
341
342 brainSurf->setVisible(bemItem->isVisible());
343
344 // Set tissue type based on surface name
345 QString surfName = bemItem->text().toLower();
346 if (surfName.contains("head") || surfName.contains("skin") || surfName.contains("scalp")) {
347 brainSurf->setTissueType(BrainSurface::TissueSkin);
348 } else if (surfName.contains("outer") && surfName.contains("skull")) {
349 brainSurf->setTissueType(BrainSurface::TissueOuterSkull);
350 } else if (surfName.contains("inner") && surfName.contains("skull")) {
351 brainSurf->setTissueType(BrainSurface::TissueInnerSkull);
352 } else if (surfName.contains("skull")) {
353 brainSurf->setTissueType(BrainSurface::TissueOuterSkull); // Default skull to outer
354 } else if (surfName.contains("brain")) {
355 brainSurf->setTissueType(BrainSurface::TissueBrain);
356 }
357
358 m_itemSurfaceMap[item] = brainSurf;
359
360 // Legacy map support (Use item text e.g. "bem_head")
361 m_surfaces["bem_" + bemItem->text()] = brainSurf;
362 }
363
364 // Handle Sensor Items
365 if (absItem && absItem->type() == AbstractTreeItem::itemTypeId(AbstractTreeItem::SensorItem)) {
366 SensorTreeItem* sensItem = static_cast<SensorTreeItem*>(absItem);
367
368 std::shared_ptr<BrainSurface> brainSurf;
369
370 QString parentText = "";
371 if (sensItem->parent())
372 parentText = sensItem->parent()->text();
373
374 if (parentText.contains("MEG/Grad") && sensItem->hasOrientation()) {
375 brainSurf = MeshFactory::createBarbell(sensItem->position(), sensItem->orientation(),
376 itemColor(*sensItem), sensItem->scale());
377 } else if (parentText.contains("MEG/Mag") && sensItem->hasOrientation()) {
378 brainSurf = MeshFactory::createPlate(sensItem->position(), sensItem->orientation(),
379 itemColor(*sensItem), sensItem->scale());
380 } else {
381 // EEG and other sensors: smooth icosphere
382 brainSurf = MeshFactory::createSphere(sensItem->position(), sensItem->scale(),
383 itemColor(*sensItem));
384 }
385
386 brainSurf->setVisible(sensItem->isVisible());
387 m_itemSurfaceMap[item] = brainSurf;
388
389
390 // Legacy map support
391 const QString keyPrefix = SURFACEKEYS::sensorParentToKeyPrefix(parentText);
392
393 QString key = keyPrefix + sensItem->text() + "_" + QString::number((quintptr)sensItem);
394 m_surfaces[key] = brainSurf;
395 }
396
397 // Handle Dipole Items
398 if (absItem && absItem->type() == AbstractTreeItem::itemTypeId(AbstractTreeItem::DipoleItem)) {
399 DipoleTreeItem* dipItem = static_cast<DipoleTreeItem*>(absItem);
400 auto dipObject = std::make_shared<DipoleObject>();
401 dipObject->load(dipItem->ecdSet());
402 dipObject->setVisible(dipItem->isVisible());
403 dipObject->applyTransform(itemTransform(item));
404
405 m_itemDipoleMap[item] = dipObject;
406 }
407
408 // Handle Source Space Items (one item per hemisphere, batched mesh)
410 SourceSpaceTreeItem* srcItem = static_cast<SourceSpaceTreeItem*>(absItem);
411 const QVector<QVector3D>& positions = srcItem->positions();
412 if (positions.isEmpty())
413 continue;
414
415 auto brainSurf = MeshFactory::createBatchedSpheres(positions, srcItem->scale(),
416 itemColor(*srcItem));
417 brainSurf->setVisible(srcItem->isVisible());
418 m_itemSurfaceMap[item] = brainSurf;
419
420 QString key = "srcsp_" + srcItem->text();
421 m_surfaces[key] = brainSurf;
422 }
423
424 // Handle Digitizer Items (batched sphere mesh per category)
426 DigitizerTreeItem* digItem = static_cast<DigitizerTreeItem*>(absItem);
427 const QVector<QVector3D>& positions = digItem->positions();
428 if (positions.isEmpty())
429 continue;
430
431 auto brainSurf = MeshFactory::createBatchedSpheres(positions, digItem->scale(),
432 itemColor(*digItem));
433 brainSurf->setVisible(digItem->isVisible());
434
435 m_itemSurfaceMap[item] = brainSurf;
436
437 // Category name for surface-map key. A monotonic counter
438 // suffix guarantees uniqueness so that multiple items of the
439 // same PointKind each get their own entry in m_surfaces.
440 // shouldRenderSurface() matches by prefix ("dig_cardinal",
441 // "dig_hpi", …) so the suffix is transparent to visibility.
442 static int s_digKeyCounter = 0;
443 QString catName;
444 switch (digItem->pointKind()) {
446 catName = "cardinal";
447 break;
449 catName = "hpi";
450 break;
452 catName = "eeg";
453 break;
455 catName = "extra";
456 break;
457 default:
458 catName = digItem->text().toLower().replace(' ', '_');
459 break;
460 }
461 QString key = QStringLiteral("dig_%1_%2").arg(catName).arg(s_digKeyCounter++);
462 m_surfaces[key] = brainSurf;
463 }
464
465 if (m_itemSurfaceMap.contains(item))
466 m_itemSurfaceMap[item]->applyTransform(itemTransform(item));
467
468 // Check children recursively
469 if (m_model->hasChildren(index)) {
470 onRowsInserted(index, 0, m_model->rowCount(index) - 1);
471 }
472 }
473 updateInflatedSurfaceTransforms();
474 updateSceneBounds();
475 m_vertexCountDirty = true;
476 m_sceneDirty = true;
477 update();
478}
479
480void BrainView::onDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector<int>& roles)
481{
482 bool transformChanged = false;
483 for (int i = topLeft.row(); i <= bottomRight.row(); ++i) {
484 QModelIndex index = m_model->index(i, 0, topLeft.parent());
485 QStandardItem* item = m_model->itemFromIndex(index);
486
487 if (m_itemSurfaceMap.contains(item)) {
488 auto surf = m_itemSurfaceMap[item];
489
490 AbstractTreeItem* absItem = dynamic_cast<AbstractTreeItem*>(item);
491 if (absItem) {
492 if (roles.contains(AbstractTreeItem::VisibleRole)) {
493 surf->setVisible(absItem->isVisible());
494 m_vertexCountDirty = true;
495 }
496 if ((roles.contains(AbstractTreeItem::ColorRole) || roles.contains(AbstractTreeItem::AlphaRole)) && absItem->color().isValid()) {
497 surf->setColor(itemColor(*absItem));
498 }
499 if (roles.contains(AbstractTreeItem::TransformRole)) {
500 surf->applyTransform(itemTransform(item));
501 transformChanged = true;
502 }
503 if (roles.contains(SurfaceTreeItem::AnnotationDataRole)) {
504 SurfaceTreeItem* sItem = static_cast<SurfaceTreeItem*>(absItem);
505 if (!sItem->annotationData().isEmpty()) {
506 surf->addAnnotation(sItem->annotationData());
507 }
508 }
509 }
510 }
511
512 if (m_itemDipoleMap.contains(item)) {
513 auto* absItem = dynamic_cast<AbstractTreeItem*>(item);
514 if (absItem && roles.contains(AbstractTreeItem::VisibleRole))
515 m_itemDipoleMap[item]->setVisible(absItem->isVisible());
516 if (roles.contains(AbstractTreeItem::TransformRole))
517 m_itemDipoleMap[item]->applyTransform(itemTransform(item));
518 }
519 }
520 if (transformChanged) {
521 updateInflatedSurfaceTransforms();
522 m_vertexCountDirty = true;
523 }
524 updateSceneBounds();
525 m_sceneDirty = true;
526 update();
527}
528
529//=============================================================================================================
530
531void BrainView::setActiveSurface(const QString& type)
532{
533 subViewForTarget(m_visualizationEditTarget).surfaceType = type;
534
535 m_activeSurfaceType = type;
536
537 // Update m_activeSurface pointer to one of the matching surfaces for stats/helpers
538 QString key = "lh_" + type;
539 if (m_surfaces.contains(key))
540 m_activeSurface = m_surfaces[key];
541 else {
542 key = "rh_" + type;
543 if (m_surfaces.contains(key))
544 m_activeSurface = m_surfaces[key];
545 }
546
547 updateInflatedSurfaceTransforms();
548 saveMultiViewSettings();
549
550 updateSceneBounds();
551 m_vertexCountDirty = true;
552 m_sceneDirty = true;
553 update();
554}
555
556void BrainView::updateSceneBounds()
557{
558 QVector3D min(std::numeric_limits<float>::max(), std::numeric_limits<float>::max(), std::numeric_limits<float>::max());
559 QVector3D max(std::numeric_limits<float>::lowest(), std::numeric_limits<float>::lowest(), std::numeric_limits<float>::lowest());
560 bool hasContent = false;
561
562 // Iterate over all surfaces
563 for (auto it = m_surfaces.begin(); it != m_surfaces.end(); ++it) {
564 if (it.value()->isVisible()) {
565 QVector3D sMin, sMax;
566 it.value()->boundingBox(sMin, sMax);
567
568 min.setX(std::min(min.x(), sMin.x()));
569 min.setY(std::min(min.y(), sMin.y()));
570 min.setZ(std::min(min.z(), sMin.z()));
571
572 max.setX(std::max(max.x(), sMax.x()));
573 max.setY(std::max(max.y(), sMax.y()));
574 max.setZ(std::max(max.z(), sMax.z()));
575 hasContent = true;
576 }
577 }
578
579 for (auto it = m_itemDipoleMap.cbegin(); it != m_itemDipoleMap.cend(); ++it) {
580 QVector3D dMin, dMax;
581 if (it.value()->isVisible() && it.value()->boundingBox(dMin, dMax)) {
582 min = QVector3D(std::min(min.x(), dMin.x()), std::min(min.y(), dMin.y()), std::min(min.z(), dMin.z()));
583 max = QVector3D(std::max(max.x(), dMax.x()), std::max(max.y(), dMax.y()), std::max(max.z(), dMax.z()));
584 hasContent = true;
585 }
586 }
587
588 if (hasContent) {
589 m_sceneCenter = (min + max) * 0.5f;
590
591 QVector3D diag = max - min;
592 m_sceneSize = std::max(diag.x(), std::max(diag.y(), diag.z()));
593
594 // Ensure non-zero size
595 if (m_sceneSize < 0.01f)
596 m_sceneSize = 0.3f;
597
598 } else {
599 // Default
600 m_sceneCenter = QVector3D(0, 0, 0);
601 m_sceneSize = 0.3f;
602 }
603}
604
605//=============================================================================================================
606
607void BrainView::setShaderMode(const QString& modeName)
608{
609 const BrainRenderer::ShaderMode mode = shaderModeFromName(modeName);
610 subViewForTarget(m_visualizationEditTarget).brainShader = mode;
611
612 m_brainShaderMode = mode;
613 saveMultiViewSettings();
614 m_sceneDirty = true;
615 update();
617}
618
619//=============================================================================================================
620
622{
623 const int prev = m_visualizationEditTarget;
624 m_visualizationEditTarget = normalizedVisualizationTarget(target, static_cast<int>(m_subViews.size()) - 1);
625
626 const SubView& sv = subViewForTarget(m_visualizationEditTarget);
627 m_activeSurfaceType = sv.surfaceType;
628 m_brainShaderMode = sv.brainShader;
629 m_bemShaderMode = sv.bemShader;
630 m_currentVisMode = sv.overlayMode;
631 const ViewVisibilityProfile& visibility = sv.visibility;
632
633 const bool remapMegSurface = (m_fieldMapper.megFieldMapOnHead() != visibility.megFieldMapOnHead);
634 m_fieldMapper.setMegFieldMapOnHead(visibility.megFieldMapOnHead);
635 m_dipolesVisible = visibility.dipoles;
636 m_networkVisible = visibility.network;
637
638 // Note: we intentionally do NOT call setVisualizationMode() on surfaces
639 // here. Each viewport's overlay mode is sent as a per-draw shader
640 // uniform (sceneData.overlayMode), so the surface objects must keep
641 // their vertex data intact — in particular the STC colour channel —
642 // regardless of which viewport is currently selected for editing.
643
644 if (m_fieldMapper.isLoaded()) {
645 if (remapMegSurface) {
646 m_fieldMapper.buildMapping(m_surfaces, m_headToMriTrans, m_applySensorTrans);
647 }
648 m_fieldMapper.apply(m_surfaces, m_singleView, m_subViews);
649 }
650
651 // Update viewport label highlighting
652 updateViewportLabelHighlight();
653
654 saveMultiViewSettings();
655
656 if (prev != m_visualizationEditTarget) {
657 emit visualizationEditTargetChanged(m_visualizationEditTarget);
658 }
659}
660
661//=============================================================================================================
662
664{
665 return m_visualizationEditTarget;
666}
667
668//=============================================================================================================
669
670QString BrainView::activeSurfaceForTarget(int target) const
671{
672 return subViewForTarget(target).surfaceType;
673}
674
675//=============================================================================================================
676
677QString BrainView::shaderModeForTarget(int target) const
678{
679 return shaderModeName(subViewForTarget(target).brainShader);
680}
681
682//=============================================================================================================
683
684QString BrainView::bemShaderModeForTarget(int target) const
685{
686 return shaderModeName(subViewForTarget(target).bemShader);
687}
688
689//=============================================================================================================
690
691QString BrainView::overlayModeForTarget(int target) const
692{
693 return visualizationModeName(subViewForTarget(target).overlayMode);
694}
695
696//=============================================================================================================
697
698ViewVisibilityProfile& BrainView::visibilityProfileForTarget(int target)
699{
700 return subViewForTarget(target).visibility;
701}
702
703//=============================================================================================================
704
705const ViewVisibilityProfile& BrainView::visibilityProfileForTarget(int target) const
706{
707 return subViewForTarget(target).visibility;
708}
709
710//=============================================================================================================
711
712SubView& BrainView::subViewForTarget(int target)
713{
714 const int normalized = normalizedVisualizationTarget(target, static_cast<int>(m_subViews.size()) - 1);
715 return (normalized < 0) ? m_singleView : m_subViews[normalized];
716}
717
718//=============================================================================================================
719
720const SubView& BrainView::subViewForTarget(int target) const
721{
722 const int normalized = normalizedVisualizationTarget(target, static_cast<int>(m_subViews.size()) - 1);
723 return (normalized < 0) ? m_singleView : m_subViews[normalized];
724}
725
726//=============================================================================================================
727
728// Note: SubView::isBrainSurfaceKey, matchesSurfaceType, shouldRenderSurface,
729// and applyOverlayToSurfaces are defined in core/viewstate.cpp.
730
731//=============================================================================================================
732
733bool BrainView::objectVisibleForTarget(const QString& object, int target) const
734{
735 return visibilityProfileForTarget(target).isObjectVisible(object);
736}
737
738//=============================================================================================================
739
741{
742 return visibilityProfileForTarget(target).megFieldMapOnHead;
743}
744
745//=============================================================================================================
746
747void BrainView::updateInflatedSurfaceTransforms()
748{
749 const bool needsInflated = (m_singleView.surfaceType == "inflated") || std::any_of(m_subViews.cbegin(), m_subViews.cend(), [](const SubView& sv) { return sv.surfaceType == "inflated"; });
750
751 const QString lhKey = "lh_inflated";
752 const QString rhKey = "rh_inflated";
753
754 if (!m_surfaces.contains(lhKey) || !m_surfaces.contains(rhKey)) {
755 return;
756 }
757
758 auto lhSurf = m_surfaces[lhKey];
759 auto rhSurf = m_surfaces[rhKey];
760
761 lhSurf->applyTransform(itemTransform(m_itemSurfaceMap.key(lhSurf)));
762 rhSurf->applyTransform(itemTransform(m_itemSurfaceMap.key(rhSurf)));
763
764 if (!needsInflated) {
765 return;
766 }
767
768 const float lhMaxX = lhSurf->maxX();
769 const float rhMinX = rhSurf->minX();
770
771 const float gap = 0.005f;
772 const float lhOffset = -gap / 2.0f - lhMaxX;
773 const float rhOffset = gap / 2.0f - rhMinX;
774
775 lhSurf->translateX(lhOffset);
776 rhSurf->translateX(rhOffset);
777}
778
779void BrainView::setBemShaderMode(const QString& modeName)
780{
781 const BrainRenderer::ShaderMode mode = shaderModeFromName(modeName);
782
783 subViewForTarget(m_visualizationEditTarget).bemShader = mode;
784
785 m_bemShaderMode = mode;
786 saveMultiViewSettings();
787 m_sceneDirty = true;
788 update();
789}
790
791//=============================================================================================================
792
794{
795 m_singleView.bemShader = m_singleView.brainShader;
796 for (int i = 0; i < m_subViews.size(); ++i) {
797 m_subViews[i].bemShader = m_subViews[i].brainShader;
798 }
799
800 m_bemShaderMode = subViewForTarget(m_visualizationEditTarget).bemShader;
801
802 saveMultiViewSettings();
803 m_sceneDirty = true;
804 update();
805}
806
807void BrainView::setSensorVisible(const QString& type, bool visible)
808{
809 const QString object = SURFACEKEYS::sensorTypeToObjectKey(type);
810 if (object.isEmpty())
811 return;
812
813 auto& profile = visibilityProfileForTarget(m_visualizationEditTarget);
814 profile.setObjectVisible(object, visible);
815
816 // Cascade parent toggle to child sub-types so that e.g. "MEG" also
817 // enables/disables MEG/Grad and MEG/Mag sub-types.
818 // Note: MEG Helmet has its own independent checkbox and is NOT cascaded.
819 if (type == QLatin1String("MEG")) {
820 profile.sensMegGrad = visible;
821 profile.sensMegMag = visible;
822 } else if (type == QLatin1String("EEG")) {
823 // No sub-types for EEG currently, but keep symmetric.
824 } else if (type == QLatin1String("Digitizer")) {
825 profile.digCardinal = visible;
826 profile.digHpi = visible;
827 profile.digEeg = visible;
828 profile.digExtra = visible;
829 }
830
831 saveMultiViewSettings();
832 m_sceneDirty = true;
833 update();
834}
835
837{
838 if (m_applySensorTrans != enabled) {
839 m_applySensorTrans = enabled;
840 refreshSensorTransforms();
841 m_sceneDirty = true;
842 update();
843 }
844}
845
846//=============================================================================================================
847
848void BrainView::setMegHelmetOverride(const QString& path)
849{
850 m_megHelmetOverridePath = path;
851}
852
854{
855 auto& profile = visibilityProfileForTarget(m_visualizationEditTarget);
856 profile.dipoles = visible;
857 m_dipolesVisible = visible;
858 saveMultiViewSettings();
859 m_sceneDirty = true;
860 update();
861}
862
863//=============================================================================================================
864
865void BrainView::setVisualizationMode(const QString& modeName)
866{
868 SubView& sv = subViewForTarget(m_visualizationEditTarget);
869 sv.overlayMode = mode;
870
871 m_currentVisMode = mode;
872
873 // Propagate the mode to brain hemisphere surfaces only (lh_*, rh_*)
874 // so that the primary colour channel holds the right data: curvature
875 // grays for Scientific or STC colours for SourceEstimate.
876 // BEM, sensor, and source-space surfaces are left untouched.
877 for (auto it = m_surfaces.begin(); it != m_surfaces.end(); ++it) {
878 const QString& key = it.key();
879 if (key.startsWith("lh_") || key.startsWith("rh_")) {
880 it.value()->setVisualizationMode(mode);
881 }
882 }
883
884 saveMultiViewSettings();
885 m_sceneDirty = true;
886 update();
887}
888
889//=============================================================================================================
890
891void BrainView::setHemiVisible(int hemiIdx, bool visible)
892{
893 auto& profile = visibilityProfileForTarget(m_visualizationEditTarget);
894 if (hemiIdx == 0) {
895 profile.lh = visible;
896 } else if (hemiIdx == 1) {
897 profile.rh = visible;
898 }
899 saveMultiViewSettings();
900 m_sceneDirty = true;
901 update();
902}
903
904//=============================================================================================================
905
906void BrainView::setBemVisible(const QString& name, bool visible)
907{
908 auto& profile = visibilityProfileForTarget(m_visualizationEditTarget);
909 profile.setObjectVisible("bem_" + name, visible);
910 saveMultiViewSettings();
911 m_sceneDirty = true;
912 update();
913}
914
916{
917 for (auto it = m_surfaces.begin(); it != m_surfaces.end(); ++it) {
918 if (it.key().startsWith("bem_")) {
919 it.value()->setUseDefaultColor(enabled);
920 }
921 }
922 m_sceneDirty = true;
923 update();
924}
925
926//=============================================================================================================
927
929{
930 m_lightingEnabled = enabled;
931 m_sceneDirty = true;
932 update();
933}
934
935//=============================================================================================================
936
938{
939 if (!enabled) {
940 if (m_pAutoRotateTimer) {
941 m_pAutoRotateTimer->stop();
942 }
943 return;
944 }
945
946 if (!m_pAutoRotateTimer) {
947 m_pAutoRotateTimer = new QTimer(this);
948 m_pAutoRotateTimer->setInterval(33);
949
950 connect(m_pAutoRotateTimer, &QTimer::timeout, this, [this]() {
951 // Rotate the camera rather than the objects, so that every surface
952 // keeps its own coregistration transform and head and sensors stay
953 // aligned with each other.
954 const QPoint step(2, 0);
955
956 if (m_viewMode == MultiView) {
957 for (int i = 0; i < m_subViews.size(); ++i) {
958 if (multiViewPresetIsPerspective(std::clamp(m_subViews[i].preset, 0, 6))) {
959 CameraController::applyMouseRotation(step, m_subViews[i].perspectiveRotation);
960 }
961 }
962 } else {
963 CameraController::applyMouseRotation(step, m_cameraRotation);
964 }
965
966 m_sceneDirty = true;
967 update();
968 });
969 }
970
971 m_pAutoRotateTimer->start();
972}
973
974//=============================================================================================================
975
977{
978 return m_pAutoRotateTimer && m_pAutoRotateTimer->isActive();
979}
980
981//=============================================================================================================
982
983void BrainView::setHeadMovementPath(const QVector<Eigen::Vector3f>& vecPositions)
984{
985 // A path needs at least one segment to be meaningful.
986 if (vecPositions.size() < 2) {
988 return;
989 }
990
991 if (!m_headPath) {
992 m_headPath = std::make_unique<PolylineObject>();
993 }
994
995 m_headPath->setPoints(vecPositions);
996 m_headPath->setVisible(true);
997
998 m_sceneDirty = true;
999 update();
1000}
1001
1002//=============================================================================================================
1003
1005{
1006 if (!m_headPath) {
1007 return;
1008 }
1009
1010 m_headPath.reset();
1011 m_sceneDirty = true;
1012 update();
1013}
1014
1015//=============================================================================================================
1016
1018{
1019 const QDateTime now = QDateTime::currentDateTime();
1020 return takeScreenshot(QStringLiteral("./Screenshots/%1-%2-3DView.png").arg(now.toString(QStringLiteral("yyyy_MM_dd")), now.toString(QStringLiteral("hh_mm_ss"))));
1021}
1022
1023//=============================================================================================================
1024
1025bool BrainView::takeScreenshot(const QString& fileName)
1026{
1027 const QImage frame = grabFramebuffer();
1028 if (frame.isNull()) {
1029 qWarning() << "[BrainView::takeScreenshot] No rendered frame - nothing written to" << fileName;
1030 return false;
1031 }
1032 if (!QDir().mkpath(QFileInfo(fileName).absolutePath()) || !frame.save(fileName)) {
1033 qWarning() << "[BrainView::takeScreenshot] Cannot write" << fileName;
1034 return false;
1035 }
1036 return true;
1037}
1038
1039//=============================================================================================================
1040
1041bool BrainView::savePng(const QString& path, int width, int height,
1042 const QString& surfaceType)
1043{
1044 // Realise the widget off-screen — QRhi init still runs because
1045 // the widget is technically "shown", but no window-manager surface
1046 // is created. Same pattern as skigen-plot Figure::save().
1047 setAttribute(Qt::WA_DontShowOnScreen, true);
1048 resize(width, height);
1049
1050 // Set the surface type filter so the render loop matches the
1051 // loaded surface names (e.g. "white" vs default "pial").
1052 m_singleView.surfaceType = surfaceType;
1053
1054 show();
1055
1056 // Spin the event loop until QRhi is initialised, at least one
1057 // frame has been rendered (m_renderer becomes non-null), and the
1058 // surface map is populated.
1059 QElapsedTimer timer;
1060 timer.start();
1061 while (timer.elapsed() < 5000) {
1062 QCoreApplication::processEvents(QEventLoop::AllEvents, 16);
1063 if (m_renderer && !m_surfaces.isEmpty())
1064 break;
1065 }
1066
1067 if (!m_renderer) {
1068 hide();
1069 setAttribute(Qt::WA_DontShowOnScreen, false);
1070 return false;
1071 }
1072
1073 // Force a dirty scene so the next grab renders everything.
1074 m_sceneDirty = true;
1075 updateSceneBounds();
1076
1077 // Pump a few more frames so pipeline resources are fully created.
1078 for (int i = 0; i < 5; ++i)
1079 QCoreApplication::processEvents(QEventLoop::AllEvents, 16);
1080
1081 // QRhiWidget::grabFramebuffer() internally creates an offscreen
1082 // frame, calls render(), and reads back the texture.
1083 QImage img = grabFramebuffer();
1084
1085 hide();
1086 setAttribute(Qt::WA_DontShowOnScreen, false);
1087
1088 if (img.isNull())
1089 return false;
1090
1091 return img.save(path, "PNG");
1092}
1093
1094//=============================================================================================================
1095
1097{
1098 m_viewMode = SingleView;
1099 m_isDraggingSplitter = false;
1100 m_activeSplitter = SplitterHit::None;
1101 unsetCursor();
1102 saveMultiViewSettings();
1103 updateViewportSeparators();
1104 updateOverlayLayout();
1105 m_sceneDirty = true;
1106 update();
1107}
1108
1109//=============================================================================================================
1110
1112{
1113 m_viewMode = MultiView;
1114 saveMultiViewSettings();
1115 updateViewportSeparators();
1116 updateOverlayLayout();
1117 m_sceneDirty = true;
1118 update();
1119}
1120
1121//=============================================================================================================
1122
1124{
1125 count = std::clamp(count, 1, static_cast<int>(m_subViews.size()));
1126 m_viewCount = count;
1127
1128 if (count == 1) {
1129 m_viewMode = SingleView;
1130 m_isDraggingSplitter = false;
1131 m_activeSplitter = SplitterHit::None;
1132 unsetCursor();
1134 } else {
1135 m_viewMode = MultiView;
1136 // Default edit target to first pane when entering multi-view
1137 if (m_visualizationEditTarget < 0)
1139 }
1140
1141 // Enable first N sub-views, disable the rest
1142 for (int i = 0; i < m_subViews.size(); ++i)
1143 m_subViews[i].enabled = (i < count);
1144
1145 saveMultiViewSettings();
1146 updateViewportSeparators();
1147 updateOverlayLayout();
1148 m_sceneDirty = true;
1149 update();
1150 emit viewCountChanged(m_viewCount);
1151}
1152
1153//=============================================================================================================
1154
1156{
1157 m_layout.resetSplits();
1158 m_multiSplitX = m_layout.splitX();
1159 m_multiSplitY = m_layout.splitY();
1160 saveMultiViewSettings();
1161 updateViewportSeparators();
1162 updateOverlayLayout();
1163 m_sceneDirty = true;
1164 update();
1165}
1166
1167bool BrainView::isViewportEnabled(int index) const
1168{
1169 if (index < 0 || index >= m_subViews.size()) {
1170 return false;
1171 }
1172
1173 return m_subViews[index].enabled;
1174}
1175
1176//=============================================================================================================
1177
1178int BrainView::enabledViewportCount() const
1179{
1180 if (m_viewMode != MultiView) {
1181 return 1;
1182 }
1183
1184 int numEnabled = 0;
1185 for (int i = 0; i < m_subViews.size(); ++i) {
1186 if (m_subViews[i].enabled) {
1187 ++numEnabled;
1188 }
1189 }
1190
1191 return numEnabled > 0 ? numEnabled : 1;
1192}
1193
1194//=============================================================================================================
1195
1196QVector<int> BrainView::enabledViewportIndices() const
1197{
1198 QVector<int> vps;
1199 if (m_viewMode == MultiView) {
1200 for (int i = 0; i < m_subViews.size(); ++i) {
1201 if (m_subViews[i].enabled)
1202 vps.append(i);
1203 }
1204 if (vps.isEmpty())
1205 vps.append(0);
1206 } else {
1207 vps.append(0);
1208 }
1209 return vps;
1210}
1211
1212//=============================================================================================================
1213
1214int BrainView::viewportIndexAt(const QPoint& pos) const
1215{
1216 if (m_viewMode != MultiView) {
1217 return 0;
1218 }
1219
1220 const auto enabledViewports = enabledViewportIndices();
1221 return m_layout.viewportIndexAt(pos, enabledViewports, size());
1222}
1223
1224//=============================================================================================================
1225
1226QRect BrainView::multiViewSlotRect(int slot, int numEnabled, const QSize& outputSize) const
1227{
1228 return m_layout.slotRect(slot, numEnabled, outputSize);
1229}
1230
1231//=============================================================================================================
1232
1233SplitterHit BrainView::hitTestSplitter(const QPoint& pos, int numEnabled, const QSize& outputSize) const
1234{
1235 if (m_viewMode != MultiView || numEnabled <= 1) {
1236 return SplitterHit::None;
1237 }
1238 return m_layout.hitTestSplitter(pos, numEnabled, outputSize);
1239}
1240
1241//=============================================================================================================
1242
1243void BrainView::updateSplitterCursor(const QPoint& pos)
1244{
1245 const SplitterHit hit = hitTestSplitter(pos, enabledViewportCount(), size());
1246 const Qt::CursorShape shape = MultiViewLayout::cursorForHit(hit);
1247 if (shape == Qt::ArrowCursor) {
1248 unsetCursor();
1249 } else {
1250 setCursor(shape);
1251 }
1252}
1253
1254//=============================================================================================================
1255
1256void BrainView::updateViewportSeparators()
1257{
1258 if (!m_verticalSeparator || !m_horizontalSeparator) {
1259 return;
1260 }
1261
1262 m_verticalSeparator->hide();
1263 m_horizontalSeparator->hide();
1264
1265 const int numEnabled = enabledViewportCount();
1266 if (m_viewMode != MultiView || numEnabled <= 1) {
1267 return;
1268 }
1269
1270 QRect vRect, hRect;
1271 m_layout.separatorGeometries(numEnabled, size(), vRect, hRect);
1272
1273 if (!vRect.isEmpty()) {
1274 m_verticalSeparator->setGeometry(vRect);
1275 m_verticalSeparator->show();
1276 m_verticalSeparator->raise();
1277 }
1278 if (!hRect.isEmpty()) {
1279 m_horizontalSeparator->setGeometry(hRect);
1280 m_horizontalSeparator->show();
1281 m_horizontalSeparator->raise();
1282 }
1283
1284 updateOverlayLayout();
1285}
1286
1287//=============================================================================================================
1288
1289void BrainView::updateOverlayLayout()
1290{
1291 const auto enabledViewports = enabledViewportIndices();
1292
1293 if (m_fpsLabel) {
1294 m_fpsLabel->setVisible(m_infoPanelVisible);
1295 m_fpsLabel->adjustSize();
1296 const int perfBottomMargin = 2;
1297
1298 if (m_viewMode == MultiView) {
1299 m_fpsLabel->move(width() - m_fpsLabel->width() - 10,
1300 height() - m_fpsLabel->height() - perfBottomMargin);
1301 } else {
1302 m_fpsLabel->move(width() - m_fpsLabel->width() - 10,
1303 height() - m_fpsLabel->height() - perfBottomMargin);
1304 }
1305
1306 m_fpsLabel->raise();
1307 }
1308
1309 if (m_singleViewInfoLabel) {
1310 const bool showSingleInfo = (m_viewMode == SingleView) && m_infoPanelVisible;
1311 m_singleViewInfoLabel->setVisible(showSingleInfo);
1312 if (showSingleInfo) {
1313 m_singleViewInfoLabel->adjustSize();
1314 m_singleViewInfoLabel->move(width() - m_singleViewInfoLabel->width() - 8, 8);
1315 m_singleViewInfoLabel->raise();
1316 }
1317 }
1318
1319 if (m_regionLabel) {
1320 const int regionY = (m_viewMode == MultiView) ? 38 : 10;
1321 m_regionLabel->move(10, regionY);
1322 if (!m_regionLabel->text().isEmpty()) {
1323 m_regionLabel->raise();
1324 }
1325 }
1326
1327 for (int i = 0; i < m_viewportNameLabels.size(); ++i) {
1328 if (m_viewportNameLabels[i]) {
1329 m_viewportNameLabels[i]->hide();
1330 }
1331 if (m_viewportInfoLabels[i]) {
1332 m_viewportInfoLabels[i]->hide();
1333 }
1334 }
1335
1336 if (m_viewMode != MultiView) {
1337 return;
1338 }
1339
1340 const int numEnabled = enabledViewports.size();
1341 const QSize overlaySize = size();
1342 for (int slot = 0; slot < numEnabled; ++slot) {
1343 const int vp = enabledViewports[slot];
1344 QLabel* label = m_viewportNameLabels[vp];
1345 QLabel* infoLabel = m_viewportInfoLabels[vp];
1346 if (!label) {
1347 continue;
1348 }
1349
1350 const int preset = std::clamp(m_subViews[vp].preset, 0, 6);
1351 label->setText(multiViewPresetName(preset));
1352
1353 const QRect pane = multiViewSlotRect(slot, numEnabled, overlaySize);
1354 label->adjustSize();
1355 label->move(pane.x() + 8, pane.y() + 8);
1356 label->setVisible(true);
1357 label->raise();
1358
1359 if (infoLabel) {
1360 infoLabel->adjustSize();
1361 infoLabel->move(pane.x() + pane.width() - infoLabel->width() - 8,
1362 pane.y() + 8);
1363 infoLabel->setVisible(m_infoPanelVisible);
1364 infoLabel->raise();
1365 }
1366 }
1367
1368 updateViewportLabelHighlight();
1369}
1370
1371//=============================================================================================================
1372
1373void BrainView::updateViewportLabelHighlight()
1374{
1375 static const QString normalStyle =
1376 QStringLiteral("color: white; font-weight: bold; font-family: sans-serif; "
1377 "font-size: 12px; background: transparent; padding: 2px 4px;");
1378 static const QString selectedStyle =
1379 QStringLiteral("color: #FFD54F; font-weight: bold; font-family: sans-serif; "
1380 "font-size: 13px; background: rgba(255,213,79,40); "
1381 "border: 1px solid #FFD54F; border-radius: 3px; padding: 2px 6px;");
1382
1383 for (int i = 0; i < m_viewportNameLabels.size(); ++i) {
1384 if (!m_viewportNameLabels[i])
1385 continue;
1386 const bool selected = (m_viewMode == MultiView && m_visualizationEditTarget == i);
1387 m_viewportNameLabels[i]->setStyleSheet(selected ? selectedStyle : normalStyle);
1388 m_viewportNameLabels[i]->adjustSize();
1389 }
1390}
1391
1392//=============================================================================================================
1393
1394void BrainView::logPerspectiveRotation(const QString& context) const
1395{
1396 Q_UNUSED(context);
1397}
1398
1399//=============================================================================================================
1400
1401void BrainView::loadMultiViewSettings()
1402{
1403 QSettings settings;
1404 settings.beginGroup(QStringLiteral("BrainView"));
1405
1406 m_multiSplitX = settings.value("multiSplitX", 0.5f).toFloat();
1407 m_multiSplitY = settings.value("multiSplitY", 0.5f).toFloat();
1408
1409 const int savedViewMode = settings.value("viewMode", static_cast<int>(SingleView)).toInt();
1410 m_viewCount = std::clamp(settings.value("viewCount", 1).toInt(), 1, static_cast<int>(m_subViews.size()));
1411 // showSingleView keeps the pane count for the next showMultiView; one pane is always the single view
1412 m_viewMode = (savedViewMode == static_cast<int>(MultiView) && m_viewCount > 1) ? MultiView : SingleView;
1413
1414 const bool hasCameraQuat = settings.contains("cameraRotW") && settings.contains("cameraRotX") && settings.contains("cameraRotY") && settings.contains("cameraRotZ");
1415 if (hasCameraQuat) {
1416 const float w = settings.value("cameraRotW", 1.0f).toFloat();
1417 const float x = settings.value("cameraRotX", 0.0f).toFloat();
1418 const float y = settings.value("cameraRotY", 0.0f).toFloat();
1419 const float z = settings.value("cameraRotZ", 0.0f).toFloat();
1420 m_cameraRotation = QQuaternion(w, x, y, z);
1421 if (m_cameraRotation.lengthSquared() <= std::numeric_limits<float>::epsilon()) {
1422 m_cameraRotation = QQuaternion();
1423 } else {
1424 m_cameraRotation.normalize();
1425 }
1426 }
1427
1428 // Reset per-index defaults, then load saved state on top
1429 for (int i = 0; i < m_subViews.size(); ++i) {
1430 m_subViews[i] = SubView::defaultForIndex(i);
1431 m_subViews[i].enabled = (i < m_viewCount);
1432 }
1433
1434 // Delegate per-SubView serialization
1435 m_singleView.load(settings, "single_", m_cameraRotation);
1436 for (int i = 0; i < m_subViews.size(); ++i)
1437 m_subViews[i].load(settings, QStringLiteral("multi%1_").arg(i), m_cameraRotation);
1438
1439 const int maxIdx = static_cast<int>(m_subViews.size()) - 1;
1440 m_visualizationEditTarget = normalizedVisualizationTarget(
1441 settings.value("visualizationEditTarget", -1).toInt(), maxIdx);
1442
1443 m_infoPanelVisible = settings.value("infoPanelVisible", true).toBool();
1444
1445 settings.endGroup();
1446
1447 m_multiSplitX = std::clamp(m_multiSplitX, 0.15f, 0.85f);
1448 m_multiSplitY = std::clamp(m_multiSplitY, 0.15f, 0.85f);
1449 m_layout.setSplitX(m_multiSplitX);
1450 m_layout.setSplitY(m_multiSplitY);
1451
1452 setVisualizationEditTarget(m_visualizationEditTarget);
1453
1454 // Notify observers that persisted state has been restored. Defer to
1455 // the next event-loop tick so that callers constructing BrainView
1456 // and connecting to these signals immediately afterwards still
1457 // receive the initial state — direct emits from inside the ctor
1458 // chain would fire before any external connect() call.
1459 QTimer::singleShot(0, this, [this]() {
1460 emit viewCountChanged(m_viewCount);
1461 emit shaderModeChanged(shaderModeName(m_brainShaderMode));
1462 });
1463}
1464
1465//=============================================================================================================
1466
1467void BrainView::saveMultiViewSettings() const
1468{
1469 QSettings settings;
1470 settings.beginGroup(QStringLiteral("BrainView"));
1471 settings.setValue("multiSplitX", m_multiSplitX);
1472 settings.setValue("multiSplitY", m_multiSplitY);
1473 settings.setValue("viewMode", static_cast<int>(m_viewMode));
1474 settings.setValue("viewCount", m_viewCount);
1475 settings.setValue("cameraRotW", m_cameraRotation.scalar());
1476 settings.setValue("cameraRotX", m_cameraRotation.x());
1477 settings.setValue("cameraRotY", m_cameraRotation.y());
1478 settings.setValue("cameraRotZ", m_cameraRotation.z());
1479 for (int i = 0; i < m_subViews.size(); ++i)
1480 settings.setValue(QStringLiteral("viewportEnabled%1").arg(i), m_subViews[i].enabled);
1481 settings.setValue("visualizationEditTarget", m_visualizationEditTarget);
1482 settings.setValue("infoPanelVisible", m_infoPanelVisible);
1483
1484 // Delegate per-SubView serialization
1485 m_singleView.save(settings, "single_");
1486 for (int i = 0; i < m_subViews.size(); ++i)
1487 m_subViews[i].save(settings, QStringLiteral("multi%1_").arg(i));
1488
1489 settings.endGroup();
1490}
1491
1492//=============================================================================================================
1493
1494void BrainView::setViewportEnabled(int index, bool enabled)
1495{
1496 if (index >= 0 && index < m_subViews.size()) {
1497 m_subViews[index].enabled = enabled;
1498 saveMultiViewSettings();
1499 updateViewportSeparators();
1500 updateOverlayLayout();
1501 m_sceneDirty = true;
1502 update();
1503 }
1504}
1505
1506//=============================================================================================================
1507
1508void BrainView::setViewportCameraPreset(int index, int preset)
1509{
1510 if (index < 0 || index >= static_cast<int>(m_subViews.size()))
1511 return;
1512 preset = std::clamp(preset, 0, 6);
1513 if (m_subViews[index].preset == preset)
1514 return;
1515 m_subViews[index].preset = preset;
1516 saveMultiViewSettings();
1517 updateOverlayLayout();
1518 m_sceneDirty = true;
1519 update();
1520}
1521
1522//=============================================================================================================
1523
1525{
1526 m_cameraRotation = QQuaternion();
1527 m_singleView.zoom = 0.0f;
1528 saveMultiViewSettings();
1529 m_sceneDirty = true;
1530 update();
1531}
1532
1533//=============================================================================================================
1534
1536{
1537 if (index < 0 || index >= static_cast<int>(m_subViews.size())) {
1538 return;
1539 }
1540
1541 m_subViews[index].zoom = 0.0f;
1542 m_subViews[index].pan = QVector2D();
1543 m_subViews[index].perspectiveRotation = QQuaternion();
1544 saveMultiViewSettings();
1545 m_sceneDirty = true;
1546 update();
1547}
1548
1549//=============================================================================================================
1550
1552{
1553 m_singleView = SubView{};
1554 for (int i = 0; i < m_subViews.size(); ++i) {
1555 const bool wasEnabled = m_subViews[i].enabled;
1556 m_subViews[i] = SubView::defaultForIndex(i);
1557 m_subViews[i].enabled = wasEnabled;
1558 }
1559 m_cameraRotation = QQuaternion();
1560 saveMultiViewSettings();
1561 updateOverlayLayout();
1562 m_sceneDirty = true;
1563 update();
1564}
1565
1566//=============================================================================================================
1567
1569{
1570 if (index < 0 || index >= static_cast<int>(m_subViews.size()))
1571 return -1;
1572 return std::clamp(m_subViews[index].preset, 0, 6);
1573}
1574
1575//=============================================================================================================
1576
1578{
1579 m_infoPanelVisible = visible;
1580 saveMultiViewSettings();
1581 updateOverlayLayout();
1582}
1583
1584//=============================================================================================================
1585
1586void BrainView::resizeEvent(QResizeEvent* event)
1587{
1588 QRhiWidget::resizeEvent(event);
1589 updateViewportSeparators();
1590 updateOverlayLayout();
1591}
1592
1593//=============================================================================================================
1594
1595void BrainView::initialize(QRhiCommandBuffer* cb)
1596{
1597 Q_UNUSED(cb);
1598
1599 m_renderer = std::make_unique<BrainRenderer>();
1600
1601 // Create dual render targets (clearing + preserving) sharing this
1602 // widget's color texture. Must be done here because colorTexture()
1603 // is only valid inside initialize()/render().
1604 m_renderer->ensureRenderTargets(rhi(), colorTexture(), colorTexture()->pixelSize());
1605}
1606
1607//=============================================================================================================
1608
1609void BrainView::render(QRhiCommandBuffer* cb)
1610{
1611 // Check if there is anything to render
1612 bool hasSurfaces = !m_surfaces.isEmpty();
1613 bool hasDipoles = !m_itemDipoleMap.isEmpty() || m_dipoles; // Check managed dipoles too
1614
1615 // If absolutely nothing is loaded, render black background
1616 if (!hasSurfaces && !hasDipoles) {
1617 // No surface loaded: render a black background instead of leaving the widget uninitialized
1618 if (!m_renderer) {
1619 m_renderer = std::make_unique<BrainRenderer>();
1620 }
1621 m_renderer->ensureRenderTargets(rhi(), colorTexture(), colorTexture()->pixelSize());
1622 m_renderer->initialize(rhi(), m_renderer->rtClear()->renderPassDescriptor(), sampleCount());
1623 m_renderer->beginFrame(cb);
1624 m_renderer->endPass(cb);
1625 return;
1626 }
1627
1628 // Ensure active surface pointer is valid if possible, otherwise just use first available for stats
1629 if (!m_activeSurface && !m_surfaces.isEmpty()) {
1630 m_activeSurface = m_surfaces.begin().value();
1631 }
1632
1633
1634 m_frameCount++;
1635 if (m_fpsTimer.elapsed() >= 500) {
1636 float fps = m_frameCount / (m_fpsTimer.elapsed() / 1000.0f);
1637
1638 // Recount vertices only when surface list/visibility changed
1639 if (m_vertexCountDirty) {
1640 auto countVerticesForSubView = [this](const SubView& sv) -> qint64 {
1641 qint64 total = 0;
1642 for (auto it = m_surfaces.cbegin(); it != m_surfaces.cend(); ++it) {
1643 const QString& key = it.key();
1644 auto surface = it.value();
1645 if (!surface)
1646 continue;
1647 if (!sv.shouldRenderSurface(key))
1648 continue;
1649 if (SubView::isBrainSurfaceKey(key)) {
1650 if (!sv.matchesSurfaceType(key))
1651 continue;
1652 } else {
1653 if (!surface->isVisible())
1654 continue;
1655 }
1656 total += surface->vertexCount();
1657 }
1658 return total;
1659 };
1660
1661 qint64 vCount = 0;
1662 if (m_viewMode == MultiView) {
1663 for (int vp : enabledViewportIndices()) {
1664 vCount += countVerticesForSubView(m_subViews[vp]);
1665 }
1666 } else {
1667 vCount = countVerticesForSubView(m_singleView);
1668 }
1669 m_cachedVertexCount = vCount;
1670 m_vertexCountDirty = false;
1671 }
1672
1673 m_fpsLabel->setText(QString("FPS: %1\nVertices: %2").arg(fps, 0, 'f', 1).arg(m_cachedVertexCount));
1674 updateOverlayLayout();
1675 m_fpsLabel->raise();
1676 m_frameCount = 0;
1677 m_fpsTimer.restart();
1678 }
1679
1680 // Initialize renderer
1681 m_renderer->ensureRenderTargets(rhi(), colorTexture(), colorTexture()->pixelSize());
1682 m_renderer->initialize(rhi(), m_renderer->rtClear()->renderPassDescriptor(), sampleCount());
1683
1684 // Determine viewport configuration
1685 QSize outputSize = m_renderer->rtClear()->pixelSize();
1686
1687 // Build list of enabled viewports
1688 const auto enabledViewports = enabledViewportIndices();
1689 int numEnabled = enabledViewports.size();
1690
1691 // ── Pre-render phase ────────────────────────────────────────────────
1692 // Pre-upload ALL Immutable GPU buffers BEFORE the render pass starts.
1693 // On Metal, uploading an Immutable buffer during an active render pass
1694 // forces an encoder restart which resets the viewport state. On
1695 // WebGL/GLES2, buffer create()/upload calls invoke glBindBuffer() which
1696 // silently modifies the currently-bound VAO's element-buffer binding,
1697 // corrupting previously drawn surfaces.
1698 //
1699 // By doing all static uploads here (outside any render pass), we
1700 // guarantee that the draw loop below only records Dynamic uniform
1701 // updates — those never interrupt the pass.
1702
1703 // Pre-upload every surface and dipole buffer that is dirty or new.
1704 // NOTE: Overlay modes are applied per-pane inside the render loop below
1705 // (not here), because different panes can have different overlays on the
1706 // same shared BrainSurface objects. Applying all pane overlays
1707 // sequentially here would leave only the last pane's vertex colours.
1708 {
1709 QRhiResourceUpdateBatch* preUpload = rhi()->nextResourceUpdateBatch();
1710#ifndef __EMSCRIPTEN__
1711 // WORKAROUND(QRhi-GLES2): WASM draws all geometry from merged per-category buffers;
1712 // per-surface uploads would pollute the GLES2 element-buffer bindings.
1713 for (auto it = m_surfaces.begin(); it != m_surfaces.end(); ++it) {
1714 it.value()->updateBuffers(rhi(), preUpload);
1715 }
1716 if (m_debugPointerSurface) {
1717 m_debugPointerSurface->updateBuffers(rhi(), preUpload);
1718 }
1719 for (auto it = m_itemDipoleMap.begin(); it != m_itemDipoleMap.end(); ++it) {
1720 it.value()->updateBuffers(rhi(), preUpload);
1721 }
1722 if (m_dipoles) {
1723 m_dipoles->updateBuffers(rhi(), preUpload);
1724 }
1725#endif
1726 if (m_videoOverlay && m_videoOverlay->isEnabled()) {
1727 m_renderer->prepareVideoOverlay(rhi(), preUpload, m_videoOverlay.get());
1728 }
1729
1730 // Prepare MRI slice textures and vertex data
1731 for (int i = 0; i < kMaxSliceSlots; ++i) {
1732 if (m_slices[i] && m_sliceVisible[i]) {
1733 m_renderer->prepareSlice(rhi(), preUpload, m_slices[i], i);
1734 } else {
1735 m_renderer->prepareSlice(rhi(), preUpload, nullptr, i);
1736 }
1737 }
1738
1739#ifdef __EMSCRIPTEN__
1740 // WORKAROUND(QRhi-GLES2): Single merged buffer for ALL surfaces.
1741 // The Qt QRhi GLES2/WebGL backend only renders the first
1742 // drawIndexed() per render pass. Multi-pass compositing via
1743 // PreserveColorContents is unreliable across WebGL implementations.
1744 // Merge everything into one VBO/IBO and issue one drawIndexed().
1745 {
1746 const SubView& sv = (m_viewMode == MultiView) ? m_subViews[0] : m_singleView;
1747
1748 QVector<BrainSurface*> allSurfaces;
1749
1750 // Brain surfaces (opaque, drawn first for depth)
1751 for (auto it = m_surfaces.begin(); it != m_surfaces.end(); ++it) {
1752 if (!SubView::isBrainSurfaceKey(it.key()))
1753 continue;
1754 if (!sv.matchesSurfaceType(it.key()))
1755 continue;
1756 if (!sv.shouldRenderSurface(it.key()))
1757 continue;
1758 allSurfaces.append(it.value().get());
1759 }
1760
1761 // Source space points
1762 for (auto it = m_surfaces.begin(); it != m_surfaces.end(); ++it) {
1763 if (!it.key().startsWith("srcsp_"))
1764 continue;
1765 if (!sv.shouldRenderSurface(it.key()))
1766 continue;
1767 if (!it.value()->isVisible())
1768 continue;
1769 allSurfaces.append(it.value().get());
1770 }
1771
1772 // Digitizer points
1773 for (auto it = m_surfaces.begin(); it != m_surfaces.end(); ++it) {
1774 if (!it.key().startsWith("dig_"))
1775 continue;
1776 if (!sv.shouldRenderSurface(it.key()))
1777 continue;
1778 if (!it.value()->isVisible())
1779 continue;
1780 allSurfaces.append(it.value().get());
1781 }
1782
1783 // BEM + sensors (transparent — appended last for correct blending order)
1784 for (auto it = m_surfaces.begin(); it != m_surfaces.end(); ++it) {
1785 bool isSensor = it.key().startsWith("sens_");
1786 bool isBem = it.key().startsWith("bem_");
1787 if (!isSensor && !isBem)
1788 continue;
1789 if (!sv.shouldRenderSurface(it.key()))
1790 continue;
1791 if (!it.value()->isVisible())
1792 continue;
1793 allSurfaces.append(it.value().get());
1794 }
1795
1796 m_renderer->prepareMergedSurfaces(rhi(), preUpload, allSurfaces, QStringLiteral("default"));
1797 }
1798#endif
1799
1800 // Network buffers are updated inside renderNetwork() via updateNodeBuffers/updateEdgeBuffers
1801 cb->resourceUpdate(preUpload);
1802 }
1803
1804 // ── Render passes ───────────────────────────────────────────────────
1805 m_renderer->beginFrame(cb);
1806
1807 for (int slot = 0; slot < numEnabled; ++slot) {
1808 int vp = (m_viewMode == MultiView) ? enabledViewports[slot] : 0;
1809 const SubView& sv = (m_viewMode == MultiView) ? m_subViews[vp] : m_singleView;
1810
1811 const QRect paneRect = (m_viewMode == MultiView)
1812 ? multiViewSlotRect(slot, numEnabled, outputSize)
1813 : QRect(0, 0, outputSize.width(), outputSize.height());
1814
1815 QRect renderRect = paneRect;
1816 if (m_viewMode == MultiView && numEnabled > 1) {
1817 constexpr int separatorPx = 2;
1818
1819 if (numEnabled == 2) {
1820 if (slot == 0) {
1821 renderRect.setWidth(std::max(1, renderRect.width() - separatorPx));
1822 }
1823 } else if (numEnabled == 3) {
1824 // 3-view: slot 0 = full top row, slots 1&2 = bottom row
1825 if (slot == 0) {
1826 // Top pane: no right neighbor, has bottom neighbor
1827 renderRect.setHeight(std::max(1, renderRect.height() - separatorPx));
1828 } else if (slot == 1) {
1829 // Bottom-left: has right neighbor, no bottom neighbor
1830 renderRect.setWidth(std::max(1, renderRect.width() - separatorPx));
1831 }
1832 // slot 2 (bottom-right): no insets needed
1833 } else {
1834 const int col = slot % 2;
1835 const int row = slot / 2;
1836
1837 const bool hasRightNeighbor = (col == 0) && (slot + 1 < numEnabled) && ((slot / 2) == ((slot + 1) / 2));
1838 const bool hasBottomNeighbor = (row == 0) && (slot + 2 < numEnabled);
1839
1840 if (hasRightNeighbor) {
1841 renderRect.setWidth(std::max(1, renderRect.width() - separatorPx));
1842 }
1843 if (hasBottomNeighbor) {
1844 renderRect.setHeight(std::max(1, renderRect.height() - separatorPx));
1845 }
1846 }
1847 }
1848
1849 const int viewX = renderRect.x();
1850 const int viewY = outputSize.height() - (renderRect.y() + renderRect.height());
1851 const int viewW = std::max(1, renderRect.width());
1852 const int viewH = std::max(1, renderRect.height());
1853
1854 QRhiViewport viewport(viewX, viewY, viewW, viewH);
1855 QRhiScissor scissor(viewX, viewY, viewW, viewH);
1856 const float aspectRatio = float(viewW) / float(viewH);
1857
1858 // Set viewport and scissor
1859 cb->setViewport(viewport);
1860 cb->setScissor(scissor);
1861
1862 // Calculate camera for this viewport
1863 const QVector3D effectiveCenter = m_cameraFocusOverride ? m_cameraFocusCenter : m_sceneCenter;
1864 const float effectiveSize = m_cameraFocusOverride ? m_cameraFocusSize : m_sceneSize;
1865 m_camera.setSceneCenter(effectiveCenter);
1866 m_camera.setSceneSize(effectiveSize);
1867 m_camera.setRotation(m_cameraRotation);
1868 m_camera.setZoom(m_singleView.zoom);
1869 const CameraResult cam = (m_viewMode == MultiView)
1870 ? m_camera.computeMultiView(sv, aspectRatio)
1871 : m_camera.computeSingleView(aspectRatio);
1872
1873 BrainRenderer::SceneData sceneData;
1874 sceneData.mvp = rhi()->clipSpaceCorrMatrix();
1875 sceneData.mvp *= cam.projection;
1876 sceneData.mvp *= cam.view;
1877 sceneData.mvp *= cam.model;
1878
1879 sceneData.cameraPos = cam.cameraPos;
1880 sceneData.lightDir = cam.cameraPos.normalized();
1881 sceneData.lightingEnabled = m_lightingEnabled;
1882 sceneData.viewportX = viewX;
1883 sceneData.viewportY = viewY;
1884 sceneData.viewportW = viewW;
1885 sceneData.viewportH = viewH;
1886 sceneData.scissorX = viewX;
1887 sceneData.scissorY = viewY;
1888 sceneData.scissorW = viewW;
1889 sceneData.scissorH = viewH;
1890
1891 // Per-draw overlayMode uniform — the shader selects the vertex colour
1892 // channel (curvature / annotation) so no per-pane vertex buffer
1893 // re-uploads are needed.
1894 sceneData.overlayMode = static_cast<float>(sv.overlayMode);
1895
1896 // Pass 1: Opaque Surfaces (Brain surfaces)
1897 // Use viewport-specific shader from subview
1898 BrainRenderer::ShaderMode currentShader = sv.brainShader;
1899 const QString overlayName = visualizationModeName(sv.overlayMode);
1900
1901 // Collect matched brain surface keys for this pane's info panel
1902#ifndef __EMSCRIPTEN__
1903 QStringList drawnKeys;
1904#else
1905 const QString drawnInfo = QStringLiteral("merged");
1906#endif
1907
1908 if (m_viewMode == MultiView && m_viewportInfoLabels[vp]) {
1909 m_viewportInfoLabels[vp]->setText(
1910 QString("Shader: %1\nSurface: %2\nOverlay: %3")
1911 .arg(shaderModeName(currentShader), sv.surfaceType, overlayName));
1912 } else if (m_viewMode == SingleView && m_singleViewInfoLabel) {
1913 m_singleViewInfoLabel->setText(
1914 QString("Shader: %1\nSurface: %2\nOverlay: %3")
1915 .arg(shaderModeName(currentShader), sv.surfaceType, overlayName));
1916 }
1917
1918#ifndef __EMSCRIPTEN__
1919 // WORKAROUND(QRhi-GLES2): WASM draws brain surfaces from a merged per-category buffer below.
1920 for (auto it = m_surfaces.begin(); it != m_surfaces.end(); ++it) {
1921 if (!sv.matchesSurfaceType(it.key()))
1922 continue;
1923 if (!sv.shouldRenderSurface(it.key()))
1924 continue;
1925 drawnKeys << it.key();
1926 m_renderer->renderSurface(cb, rhi(), sceneData, it.value().get(), currentShader);
1927 }
1928#endif
1929
1930#ifndef __EMSCRIPTEN__
1931 // Update info panel with drawn brain surface keys after rendering
1932 {
1933 const QString drawnInfo = drawnKeys.isEmpty() ? QStringLiteral("none") : drawnKeys.join(QStringLiteral(", "));
1934 if (m_viewMode == MultiView && m_viewportInfoLabels[vp]) {
1935 m_viewportInfoLabels[vp]->setText(m_viewportInfoLabels[vp]->text() + QStringLiteral("\nDrawn: ") + drawnInfo);
1936 } else if (m_viewMode == SingleView && m_singleViewInfoLabel) {
1937 m_singleViewInfoLabel->setText(m_singleViewInfoLabel->text() + QStringLiteral("\nDrawn: ") + drawnInfo);
1938 }
1939 }
1940#endif
1941
1942#ifdef __EMSCRIPTEN__
1943 // ══════════════════════════════════════════════════════════════════════
1944 // WORKAROUND(QRhi-GLES2): Single-pass merged rendering.
1945 // The Qt QRhi GLES2/WebGL backend only renders the first drawIndexed()
1946 // per render pass, AND multi-pass compositing via PreserveColorContents
1947 // is unreliable. All visible surfaces (brain, BEM, sensors, source
1948 // space, digitizers) are merged into one VBO/IBO and drawn in a single
1949 // drawIndexed() call in the clearing pass.
1950 //
1951 // Remove when upstream Qt fixes the QRhi GLES2 drawIndexed bug.
1952 // ══════════════════════════════════════════════════════════════════════
1953 m_renderer->drawMergedSurfaces(cb, rhi(), sceneData, currentShader, QStringLiteral("default"));
1954
1955#else
1956
1957 // ── Batched desktop rendering ───────────────────────────────────────
1958 // Single pass over m_surfaces categorises non-brain items into opaque
1959 // and transparent draw lists. All uniform uploads are batched into
1960 // one QRhiResourceUpdateBatch and submitted once, eliminating per-
1961 // surface batch allocation and redundant viewport/scissor reassertion.
1962
1963 // Determine per-viewport field-map visibility
1964 const bool megFieldVisible = sv.visibility.megFieldMap;
1965 const bool eegFieldVisible = sv.visibility.eegFieldMap;
1966 const BrainRenderer::ShaderMode currentBemShader = sv.bemShader;
1967 const QString& megFieldKey = m_fieldMapper.megSurfaceKey();
1968 const QString& eegFieldKey = m_fieldMapper.eegSurfaceKey();
1969
1970 struct DrawItem
1971 {
1972 BrainSurface* surface;
1974 float overlayMode;
1975 float distSq; // for transparent back-to-front sort
1976 int uniformOffset; // filled by prepareSurfaceDraw
1977 };
1978
1979 QVector<DrawItem> opaqueDraws;
1980 QVector<DrawItem> transparentDraws;
1981
1982 for (auto it = m_surfaces.begin(); it != m_surfaces.end(); ++it) {
1983 const QString& key = it.key();
1984 BrainSurface* surf = it.value().get();
1985
1986 if (SubView::isBrainSurfaceKey(key)) {
1987 // Brain surfaces already rendered above
1988 continue;
1989 } else if (key.startsWith("srcsp_") || key.startsWith("dig_")) {
1990 if (!sv.shouldRenderSurface(key))
1991 continue;
1992 if (!surf->isVisible())
1993 continue;
1994 if (key.startsWith(QLatin1String("dig_live_t_")) || key.startsWith(QLatin1String("dig_ray_")) || key.startsWith(QLatin1String("dig_probe_"))) {
1995 QVector3D bmin, bmax;
1996 surf->boundingBox(bmin, bmax);
1997 QVector3D ctr = (bmin + bmax) * 0.5f;
1998 float dist = (sceneData.cameraPos - ctr).lengthSquared();
1999 transparentDraws.append({surf, BrainRenderer::Holographic,
2000 static_cast<float>(BrainSurface::ModeScientific),
2001 dist, -1});
2002 } else {
2003 opaqueDraws.append({surf, currentShader,
2004 static_cast<float>(BrainSurface::ModeScientific),
2005 0.0f, -1});
2006 }
2007 } else {
2008 bool isSensor = key.startsWith("sens_");
2009 bool isBem = key.startsWith("bem_");
2010 if (!isSensor && !isBem)
2011 continue;
2012 if (!sv.shouldRenderSurface(key))
2013 continue;
2014 if (!surf->isVisible())
2015 continue;
2016
2017 QVector3D bmin, bmax;
2018 surf->boundingBox(bmin, bmax);
2019 QVector3D center = (bmin + bmax) * 0.5f;
2020 float dist = (sceneData.cameraPos - center).lengthSquared();
2021
2022 auto mode = isBem ? currentBemShader : BrainRenderer::Holographic;
2023 float itemOverlay = static_cast<float>(BrainSurface::ModeScientific);
2024 if (key == megFieldKey && !megFieldVisible)
2025 itemOverlay = static_cast<float>(BrainSurface::ModeSurface);
2026 else if (key == eegFieldKey && !eegFieldVisible)
2027 itemOverlay = static_cast<float>(BrainSurface::ModeSurface);
2028
2029 transparentDraws.append({surf, mode, itemOverlay, dist, -1});
2030 }
2031 }
2032
2033 // Sort transparent items back-to-front for correct alpha blending
2034 std::sort(transparentDraws.begin(), transparentDraws.end(),
2035 [](const DrawItem& a, const DrawItem& b) { return a.distSq > b.distSq; });
2036
2037 // Batch all uniform uploads into a single resource update
2038 QRhiResourceUpdateBatch* surfBatch = rhi()->nextResourceUpdateBatch();
2039 BrainRenderer::SceneData batchData = sceneData;
2040
2041 for (auto& item : opaqueDraws) {
2042 batchData.overlayMode = item.overlayMode;
2043 item.uniformOffset = m_renderer->prepareSurfaceDraw(surfBatch, batchData, item.surface);
2044 }
2045 for (auto& item : transparentDraws) {
2046 batchData.overlayMode = item.overlayMode;
2047 item.uniformOffset = m_renderer->prepareSurfaceDraw(surfBatch, batchData, item.surface);
2048 }
2049
2050 // Batch MRI slice uniform uploads into the same batch
2051 int sliceOffsets[kMaxSliceSlots] = {-1, -1, -1};
2052 if (sv.visibility.mriSlices) {
2053 for (int i = 0; i < kMaxSliceSlots; ++i) {
2054 if (m_slices[i] && m_sliceVisible[i]) {
2055 sliceOffsets[i] = m_renderer->prepareSliceDraw(surfBatch, sceneData, i);
2056 }
2057 }
2058 }
2059
2060 cb->resourceUpdate(surfBatch);
2061
2062 // Set viewport/scissor once for all batched draws
2063 cb->setViewport(viewport);
2064 cb->setScissor(scissor);
2065
2066 // Issue all draw calls — no resource updates or state resets between them
2067 for (const auto& item : opaqueDraws)
2068 m_renderer->issueSurfaceDraw(cb, item.surface, item.mode, item.uniformOffset);
2069
2070 // Issue MRI slice draws after opaque surfaces but before the holographic
2071 // brain. Background voxels are discarded in the shader; remaining anatomy
2072 // alpha-blends into the framebuffer. The subsequent holographic additive
2073 // pass (SrcAlpha + One) adds its glow on top without being dimmed.
2074 // depthTest=true, depthWrite=false keeps slices behind opaque geometry.
2075 for (int i = 0; i < kMaxSliceSlots; ++i) {
2076 if (sliceOffsets[i] >= 0) {
2077 m_renderer->issueSliceDraw(cb, i, sliceOffsets[i]);
2078 }
2079 }
2080
2081 BrainSurface* videoOverlayTargetSurface = nullptr;
2082 const bool hasVideoOverlay = m_videoOverlay && m_videoOverlay->isEnabled() && m_videoOverlay->hasFrame();
2083 if (hasVideoOverlay) {
2084 // Target is always the head surface (BEM head or TissueSkin).
2085 if (m_surfaces.contains(QStringLiteral("bem_head"))) {
2086 videoOverlayTargetSurface = m_surfaces[QStringLiteral("bem_head")].get();
2087 } else {
2088 for (auto it = m_surfaces.begin(); it != m_surfaces.end(); ++it) {
2089 if (it.value() && it.value()->tissueType() == BrainSurface::TissueSkin) {
2090 videoOverlayTargetSurface = it.value().get();
2091 break;
2092 }
2093 }
2094 }
2095 }
2096
2097 bool videoOverlayDrawn = false;
2098 auto drawVideoOverlay = [&]() {
2099 if (videoOverlayDrawn)
2100 return;
2101 if (videoOverlayTargetSurface) {
2102 m_renderer->renderVideoOverlayOnSurface(cb, rhi(), sceneData,
2103 m_videoOverlay.get(), videoOverlayTargetSurface);
2104 } else if (hasVideoOverlay) {
2105 m_renderer->renderVideoOverlay(cb, rhi(), sceneData, m_videoOverlay.get());
2106 }
2107 videoOverlayDrawn = true;
2108 };
2109
2110 for (const auto& item : transparentDraws) {
2111 m_renderer->issueSurfaceDraw(cb, item.surface, item.mode, item.uniformOffset);
2112 if (item.surface == videoOverlayTargetSurface) {
2113 drawVideoOverlay();
2114 }
2115 }
2116 drawVideoOverlay();
2117
2118 // Render Dipoles
2119 for (auto it = m_itemDipoleMap.begin(); it != m_itemDipoleMap.end(); ++it) {
2120 if (it.value()->isVisible() && sv.visibility.dipoles) {
2121 m_renderer->renderDipoles(cb, rhi(), sceneData, it.value().get());
2122 }
2123 }
2124
2125 if (sv.visibility.dipoles && m_dipoles) {
2126 m_renderer->renderDipoles(cb, rhi(), sceneData, m_dipoles.get());
2127 }
2128
2129 // Render Connectivity Network
2130 if (sv.visibility.network && m_network) {
2131 m_renderer->renderNetwork(cb, rhi(), sceneData, m_network.get());
2132 }
2133
2134 // Render the head movement path. It is not connectivity data, so it is not
2135 // tied to the network visibility flag.
2136 if (m_headPath) {
2137 m_renderer->renderPolyline(cb, rhi(), sceneData, m_headPath.get());
2138 }
2139
2140 // Intersection Pointer
2141 if (m_hasIntersection && m_debugPointerSurface) {
2142 BrainRenderer::SceneData debugSceneData = sceneData;
2143 debugSceneData.overlayMode = 0.0f; // pass-through for holographic shell
2144
2145 QMatrix4x4 translation;
2146 translation.translate(m_lastIntersectionPoint);
2147
2148 debugSceneData.mvp = rhi()->clipSpaceCorrMatrix() * cam.projection * cam.view * cam.model * translation;
2149
2150 m_renderer->renderSurface(cb, rhi(), debugSceneData, m_debugPointerSurface.get(), BrainRenderer::Holographic);
2151 }
2152
2153#endif // !__EMSCRIPTEN__ — end of per-surface draw path
2154
2155 } // End of viewport loop
2156
2157 m_renderer->endPass(cb);
2158
2159 // On WASM, all surfaces are drawn in the single clearing pass above
2160 // via the merged "default" group. No additional preserving passes needed.
2161}
2162
2163//=============================================================================================================
2164
2165void BrainView::mousePressEvent(QMouseEvent* e)
2166{
2167 if (e->button() == Qt::LeftButton) {
2168 m_perspectiveRotatedSincePress = false;
2169 m_draggedSincePress = false;
2170 }
2171
2172 if (e->button() == Qt::LeftButton && m_viewMode == MultiView) {
2173 const int clickedVp = viewportIndexAt(e->pos());
2174 if (clickedVp >= 0 && m_viewportNameLabels[clickedVp] && m_viewportNameLabels[clickedVp]->isVisible()) {
2175 if (m_viewportNameLabels[clickedVp]->geometry().contains(e->pos())) {
2176 if (clickedVp != m_visualizationEditTarget) {
2177 setVisualizationEditTarget(clickedVp);
2178 }
2179 showViewportPresetMenu(clickedVp, mapToGlobal(e->pos()));
2180 m_lastMousePos = e->pos();
2181 return;
2182 }
2183 }
2184
2185 const int numEnabled = enabledViewportCount();
2186 const SplitterHit hit = hitTestSplitter(e->pos(), numEnabled, size());
2187 if (hit != SplitterHit::None) {
2188 m_isDraggingSplitter = true;
2189 m_activeSplitter = hit;
2190 m_lastMousePos = e->pos();
2191 updateSplitterCursor(e->pos());
2192 return;
2193 }
2194
2195 // Select the clicked viewport as the active edit target
2196 const int clickedVpForSelection = viewportIndexAt(e->pos());
2197 if (clickedVpForSelection >= 0 && clickedVpForSelection != m_visualizationEditTarget) {
2198 setVisualizationEditTarget(clickedVpForSelection);
2199 }
2200 }
2201
2202 m_lastMousePos = e->pos();
2203}
2204
2205//=============================================================================================================
2206
2207void BrainView::mouseMoveEvent(QMouseEvent* event)
2208{
2209 if (m_isDraggingSplitter && (event->buttons() & Qt::LeftButton)) {
2210 m_layout.dragSplitter(event->pos(), m_activeSplitter, size());
2211 m_multiSplitX = m_layout.splitX();
2212 m_multiSplitY = m_layout.splitY();
2213
2214 m_lastMousePos = event->pos();
2215 updateViewportSeparators();
2216 m_sceneDirty = true;
2217 update();
2218 return;
2219 }
2220
2221 if (event->buttons() & Qt::LeftButton) {
2222 if (m_viewMode == MultiView) {
2223 const int activeVp = viewportIndexAt(event->pos());
2224 const int activePreset = (activeVp >= 0 && activeVp < m_subViews.size())
2225 ? std::clamp(m_subViews[activeVp].preset, 0, 6)
2226 : 1;
2227
2228 if (activeVp >= 0 && !multiViewPresetIsPerspective(activePreset)) {
2229 // Planar views (Top/Front/Left): pan along the view plane
2230 const QPoint diff = event->pos() - m_lastMousePos;
2231 CameraController::applyMousePan(diff, m_subViews[activeVp].pan, m_sceneSize);
2232 m_draggedSincePress = true;
2233 m_lastMousePos = event->pos();
2234 m_sceneDirty = true;
2235 update();
2236 return;
2237 }
2238
2239 if (activeVp >= 0 && multiViewPresetIsPerspective(activePreset)) {
2240 // Perspective view: rotate
2241 QPoint diff = event->pos() - m_lastMousePos;
2242 CameraController::applyMouseRotation(diff, m_subViews[activeVp].perspectiveRotation);
2243
2244 m_perspectiveRotatedSincePress = true;
2245 m_draggedSincePress = true;
2246 m_lastMousePos = event->pos();
2247 m_sceneDirty = true;
2248 update();
2249 return;
2250 }
2251
2252 m_lastMousePos = event->pos();
2253 return;
2254 }
2255
2256 // Single-view rotation
2257 QPoint diff = event->pos() - m_lastMousePos;
2258 CameraController::applyMouseRotation(diff, m_cameraRotation);
2259 m_draggedSincePress = true;
2260
2261 m_lastMousePos = event->pos();
2262 m_sceneDirty = true;
2263 update();
2264 } else {
2265 if (m_viewMode == MultiView) {
2266 updateSplitterCursor(event->pos());
2267 } else {
2268 unsetCursor();
2269 }
2270 castRay(event->pos());
2271 }
2272}
2273
2274//=============================================================================================================
2275
2276void BrainView::mouseReleaseEvent(QMouseEvent* event)
2277{
2278 if (event->button() == Qt::LeftButton && m_isDraggingSplitter) {
2279 m_isDraggingSplitter = false;
2280 m_activeSplitter = SplitterHit::None;
2281 saveMultiViewSettings();
2282 updateSplitterCursor(event->pos());
2283 return;
2284 }
2285
2286 if (event->button() == Qt::LeftButton && m_viewMode == MultiView && m_perspectiveRotatedSincePress) {
2287 m_perspectiveRotatedSincePress = false;
2288 saveMultiViewSettings();
2289 }
2290
2291 // Save pan offset after dragging in a planar viewport
2292 if (event->button() == Qt::LeftButton && m_viewMode == MultiView && !m_perspectiveRotatedSincePress) {
2293 saveMultiViewSettings();
2294 }
2295
2296 // Emit surface click if a clean left-click (no rotate or pan drag) landed on geometry
2297 if (event->button() == Qt::LeftButton && !m_draggedSincePress) {
2298 castRay(event->pos());
2299 if (m_hasIntersection) {
2300 emit surfacePointClicked(m_lastIntersectionPoint);
2301 }
2302 }
2303
2304 if (m_viewMode == MultiView) {
2305 updateSplitterCursor(event->pos());
2306 } else {
2307 unsetCursor();
2308 }
2309}
2310
2311//=============================================================================================================
2312
2313void BrainView::mouseDoubleClickEvent(QMouseEvent* event)
2314{
2315 if (event->button() == Qt::LeftButton) {
2316 castRay(event->pos());
2317 if (m_hasIntersection) {
2318 emit surfacePointDoubleClicked(m_lastIntersectionPoint);
2319 return;
2320 }
2321 }
2322 QRhiWidget::mouseDoubleClickEvent(event);
2323}
2324
2325//=============================================================================================================
2326
2327void BrainView::wheelEvent(QWheelEvent* event)
2328{
2329 const float delta = event->angleDelta().y() / 120.0f;
2330
2331 if (m_viewMode == MultiView) {
2332 const int vp = viewportIndexAt(event->position().toPoint());
2333 if (vp >= 0 && vp < m_subViews.size()) {
2334 m_subViews[vp].zoom += delta;
2335 saveMultiViewSettings();
2336 }
2337 } else {
2338 m_singleView.zoom += delta;
2339 saveMultiViewSettings();
2340 }
2341 m_sceneDirty = true;
2342 update();
2343}
2344
2345//=============================================================================================================
2346
2347void BrainView::keyPressEvent(QKeyEvent* event)
2348{
2349 if (event->key() == Qt::Key_S) {
2350 saveSnapshot();
2351 } else if (event->key() == Qt::Key_R) {
2352 m_cameraRotation = QQuaternion();
2353 logPerspectiveRotation("reset-initial");
2354 saveMultiViewSettings();
2355 m_sceneDirty = true;
2356 update();
2357 }
2358}
2359
2360//=============================================================================================================
2361
2362bool BrainView::loadSourceEstimate(const QString& lhPath, const QString& rhPath)
2363{
2364 return m_sourceManager.load(lhPath, rhPath, m_surfaces, m_activeSurfaceType);
2365}
2366
2367//=============================================================================================================
2368
2369void BrainView::onSourceEstimateLoaded(int numTimePoints)
2370{
2371 setVisualizationMode("Source Estimate");
2372 emit sourceEstimateLoaded(numTimePoints);
2373 setTimePoint(0);
2374}
2375
2376//=============================================================================================================
2377
2379{
2380 m_sourceManager.setTimePoint(index, m_surfaces, m_singleView, m_subViews);
2381 m_sceneDirty = true;
2382 update();
2383}
2384
2385//=============================================================================================================
2386
2387void BrainView::setSourceColormap(const QString& name)
2388{
2389 m_sourceManager.setColormap(name);
2390 setTimePoint(m_sourceManager.currentTimePoint());
2391}
2392
2393//=============================================================================================================
2394
2395void BrainView::setSourceThresholds(float min, float mid, float max)
2396{
2397 m_sourceManager.setThresholds(min, mid, max);
2398 setTimePoint(m_sourceManager.currentTimePoint());
2399}
2400
2401//=============================================================================================================
2402
2404{
2405 setVisualizationMode("Source Estimate");
2406 m_sourceManager.startStreaming(m_surfaces, m_singleView, m_subViews);
2407}
2408
2409//=============================================================================================================
2410
2412{
2413 m_sourceManager.stopStreaming();
2414}
2415
2416//=============================================================================================================
2417
2419{
2420 return m_sourceManager.isStreaming();
2421}
2422
2423//=============================================================================================================
2424
2425void BrainView::pushRealtimeSourceData(const Eigen::VectorXd& matData)
2426{
2427 m_sourceManager.pushData(matData);
2428}
2429
2430//=============================================================================================================
2431
2433{
2434 m_sourceManager.setInterval(msec);
2435}
2436
2437//=============================================================================================================
2438
2440{
2441 m_sourceManager.setLooping(enabled);
2442}
2443
2444//=============================================================================================================
2445
2446void BrainView::onRealtimeColorsAvailable(const QVector<uint32_t>& colorsLh,
2447 const QVector<uint32_t>& colorsRh)
2448{
2449 // Apply colors to all brain surfaces matching active surface types
2450 QSet<QString> activeTypes;
2451 activeTypes.insert(m_singleView.surfaceType);
2452 for (int i = 0; i < m_subViews.size(); ++i) {
2453 activeTypes.insert(m_subViews[i].surfaceType);
2454 }
2455
2456 for (auto it = m_surfaces.begin(); it != m_surfaces.end(); ++it) {
2457 if (!it.value() || it.value()->tissueType() != BrainSurface::TissueBrain)
2458 continue;
2459
2460 for (const QString& type : activeTypes) {
2461 if (it.key().endsWith(type)) {
2462 int hemi = it.value()->hemi();
2463 const QVector<uint32_t>& colors = (hemi == 0) ? colorsLh : colorsRh;
2464 if (!colors.isEmpty()) {
2465 it.value()->applySourceEstimateColors(colors);
2466 }
2467 break;
2468 }
2469 }
2470 }
2471
2472 m_sceneDirty = true;
2473 update();
2474}
2475
2476//=============================================================================================================
2477
2478bool BrainView::loadSensorField(const QString& evokedPath, int aveIndex)
2479{
2480 auto evoked = DataLoader::loadEvoked(evokedPath, aveIndex);
2481 if (evoked.isEmpty())
2482 return false;
2483
2484 // Preserve the current time point when switching between evoked sets
2485 // that share the same sensor configuration (same file, different condition).
2486 const int previousTimePoint = m_fieldMapper.timePoint();
2487 const bool canReuse = m_fieldMapper.hasMappingFor(evoked);
2488
2489 m_fieldMapper.setEvoked(evoked);
2490
2491 if (!canReuse) {
2492 // Sensor config changed — full rebuild required (also precomputes global range)
2493 if (!m_fieldMapper.buildMapping(m_surfaces, m_headToMriTrans, m_applySensorTrans)) {
2494 m_fieldMapper.setEvoked(FIFFLIB::FiffEvoked()); // Clear state on failure
2495 return false;
2496 }
2497 } else {
2498 // Mapping reused — recompute normalization for new evoked data
2499 m_fieldMapper.computeNormRange();
2500 }
2501
2502 // Clamp preserved time point to the range of the new evoked data
2503 const int numTimes = static_cast<int>(m_fieldMapper.evoked().times.size());
2504 const int tp = qBound(0, previousTimePoint, numTimes - 1);
2505
2506 emit sensorFieldLoaded(numTimes, tp);
2508 return true;
2509}
2510
2511//=============================================================================================================
2512
2513QStringList BrainView::probeEvokedSets(const QString& evokedPath)
2514{
2515 return DataLoader::probeEvokedSets(evokedPath);
2516}
2517
2518//=============================================================================================================
2519
2521{
2522 if (!m_fieldMapper.isLoaded() || m_fieldMapper.evoked().isEmpty()) {
2523 return;
2524 }
2525
2526 int maxIdx = static_cast<int>(m_fieldMapper.evoked().times.size()) - 1;
2527 if (maxIdx < 0) {
2528 return;
2529 }
2530
2531 m_fieldMapper.setTimePoint(qBound(0, index, maxIdx));
2532 m_fieldMapper.apply(m_surfaces, m_singleView, m_subViews);
2533 emit sensorFieldTimePointChanged(m_fieldMapper.timePoint(), m_fieldMapper.evoked().times(m_fieldMapper.timePoint()));
2534 m_sceneDirty = true;
2535 update();
2536}
2537
2538//=============================================================================================================
2539
2540void BrainView::setSensorFieldVisible(const QString& type, bool visible)
2541{
2542 auto& profile = visibilityProfileForTarget(m_visualizationEditTarget);
2543 if (type == "MEG") {
2544 profile.megFieldMap = visible;
2545 } else if (type == "EEG") {
2546 profile.eegFieldMap = visible;
2547 } else {
2548 return;
2549 }
2550
2551 saveMultiViewSettings();
2552 m_fieldMapper.apply(m_surfaces, m_singleView, m_subViews);
2553 m_sceneDirty = true;
2554 update();
2555}
2556
2557//=============================================================================================================
2558
2559void BrainView::setSensorFieldContourVisible(const QString& type, bool visible)
2560{
2561 auto& profile = visibilityProfileForTarget(m_visualizationEditTarget);
2562 if (type == "MEG") {
2563 profile.megFieldContours = visible;
2564 } else if (type == "EEG") {
2565 profile.eegFieldContours = visible;
2566 } else {
2567 return;
2568 }
2569
2570 saveMultiViewSettings();
2571 m_fieldMapper.apply(m_surfaces, m_singleView, m_subViews);
2572 m_sceneDirty = true;
2573 update();
2574}
2575
2576//=============================================================================================================
2577
2579{
2580 auto& profile = visibilityProfileForTarget(m_visualizationEditTarget);
2581 if (profile.megFieldMapOnHead == useHead && m_fieldMapper.megFieldMapOnHead() == useHead) {
2582 return;
2583 }
2584
2585 profile.megFieldMapOnHead = useHead;
2586 m_fieldMapper.setMegFieldMapOnHead(useHead);
2587 saveMultiViewSettings();
2588 if (m_fieldMapper.isLoaded()) {
2589 m_fieldMapper.buildMapping(m_surfaces, m_headToMriTrans, m_applySensorTrans);
2590 m_fieldMapper.apply(m_surfaces, m_singleView, m_subViews);
2591 m_sceneDirty = true;
2592 update();
2593 }
2594}
2595
2596//=============================================================================================================
2597
2598void BrainView::setSensorFieldColormap(const QString& name)
2599{
2600 if (m_fieldMapper.colormap() == name) {
2601 return;
2602 }
2603 m_fieldMapper.setColormap(name);
2604 m_fieldMapper.apply(m_surfaces, m_singleView, m_subViews);
2605 m_sceneDirty = true;
2606 update();
2607}
2608
2609//=============================================================================================================
2610
2612{
2613 return m_sourceManager.tstep();
2614}
2615
2616//=============================================================================================================
2617
2619{
2620 return m_sourceManager.tmin();
2621}
2622
2623//=============================================================================================================
2624
2626{
2627 return m_sourceManager.numTimePoints();
2628}
2629
2630//=============================================================================================================
2631
2633{
2634 if (!m_fieldMapper.isLoaded() || m_fieldMapper.evoked().nave == -1 || m_fieldMapper.evoked().times.size() == 0) {
2635 return -1;
2636 }
2637
2638 int bestIdx = 0;
2639 float bestDist = std::abs(m_fieldMapper.evoked().times(0) - timeSec);
2640 for (int i = 1; i < m_fieldMapper.evoked().times.size(); ++i) {
2641 float dist = std::abs(m_fieldMapper.evoked().times(i) - timeSec);
2642 if (dist < bestDist) {
2643 bestDist = dist;
2644 bestIdx = i;
2645 }
2646 }
2647 return bestIdx;
2648}
2649
2650//=============================================================================================================
2651
2652int BrainView::closestStcIndex(float timeSec) const
2653{
2654 return m_sourceManager.closestIndex(timeSec);
2655}
2656
2657//=============================================================================================================
2658
2659bool BrainView::sensorFieldTimeRange(float& tmin, float& tmax) const
2660{
2661 if (!m_fieldMapper.isLoaded() || m_fieldMapper.evoked().nave == -1 || m_fieldMapper.evoked().times.size() == 0) {
2662 return false;
2663 }
2664 tmin = m_fieldMapper.evoked().times(0);
2665 tmax = m_fieldMapper.evoked().times(m_fieldMapper.evoked().times.size() - 1);
2666 return true;
2667}
2668
2669//=============================================================================================================
2670// ── Real-time sensor data streaming ────────────────────────────────────
2671//=============================================================================================================
2672
2673void BrainView::startRealtimeSensorStreaming(const QString& modality)
2674{
2675 m_sensorStreamManager.startStreaming(modality, m_fieldMapper, m_surfaces);
2676}
2677
2678//=============================================================================================================
2679
2681{
2682 m_sensorStreamManager.stopStreaming();
2683}
2684
2685//=============================================================================================================
2686
2688{
2689 return m_sensorStreamManager.isStreaming();
2690}
2691
2692//=============================================================================================================
2693
2694void BrainView::pushRealtimeSensorData(const Eigen::VectorXf& vecData)
2695{
2696 m_sensorStreamManager.pushData(vecData);
2697}
2698
2699//=============================================================================================================
2700
2702{
2703 m_sensorStreamManager.setInterval(msec);
2704}
2705
2706//=============================================================================================================
2707
2709{
2710 m_sensorStreamManager.setLooping(enabled);
2711}
2712
2713//=============================================================================================================
2714
2716{
2717 m_sensorStreamManager.setAverages(numAvr);
2718}
2719
2720//=============================================================================================================
2721
2723{
2724 m_sensorStreamManager.setColormap(name);
2725}
2726
2727//=============================================================================================================
2728
2729void BrainView::onSensorStreamColorsAvailable(const QString& surfaceKey,
2730 const QVector<uint32_t>& colors)
2731{
2732 if (surfaceKey.isEmpty() || !m_surfaces.contains(surfaceKey)) {
2733 return;
2734 }
2735
2736 auto surface = m_surfaces[surfaceKey];
2737 if (surface && !colors.isEmpty()) {
2738 surface->applySourceEstimateColors(colors);
2739 }
2740
2741 m_sceneDirty = true;
2742 update();
2743}
2744
2745//=============================================================================================================
2746
2747bool BrainView::loadSensors(const QString& fifPath)
2748{
2749 if (!m_model)
2750 return false;
2751
2752 auto r = DataLoader::loadSensors(fifPath, m_megHelmetOverridePath);
2753 if (!r.hasInfo && !r.hasDigitizer)
2754 return false;
2755
2756 // Store Device→Head transform for later helmet surface reloads
2757 m_devHeadTrans = r.devHeadTrans;
2758 m_hasDevHead = r.hasDevHead;
2759
2760 if (!r.megGradItems.isEmpty())
2761 m_model->addSensors("MEG/Grad", r.megGradItems);
2762 if (!r.megMagItems.isEmpty())
2763 m_model->addSensors("MEG/Mag", r.megMagItems);
2764 if (!r.eegItems.isEmpty())
2765 m_model->addSensors("EEG", r.eegItems);
2766
2767 if (r.helmetSurface) {
2768 m_surfaces["sens_surface_meg"] = r.helmetSurface;
2769 } else {
2770 qWarning() << "BrainView::loadSensors: NO helmet surface returned from DataLoader!";
2771 }
2772
2773 if (!r.digitizerPoints.isEmpty())
2774 m_model->addDigitizerData(r.digitizerPoints);
2775
2776 // Extract cardinal dig points for later fiducial queries
2777 m_cardinalDigPoints.clear();
2778 for (const auto& dp : r.digitizerPoints) {
2779 if (dp.kind == FIFFV_POINT_CARDINAL)
2780 m_cardinalDigPoints.append(dp);
2781 }
2782
2783 return true;
2784}
2785
2786//=============================================================================================================
2787
2788QMap<int, QVector3D> BrainView::cardinalFiducialsInMri() const
2789{
2790 QMap<int, QVector3D> result;
2791 if (m_cardinalDigPoints.isEmpty())
2792 return result;
2793
2794 // Cardinal points are in Head coordinates; transform to MRI if available
2795 const bool haveTrans = (m_headToMriTrans.from != 0 || m_headToMriTrans.to != 0);
2796 QMatrix4x4 headToMri;
2797 headToMri.setToIdentity();
2798 if (haveTrans)
2799 headToMri = SURFACEKEYS::toQMatrix4x4(m_headToMriTrans.trans);
2800
2801 for (const auto& dp : m_cardinalDigPoints) {
2802 const QVector3D posHead(dp.r[0], dp.r[1], dp.r[2]);
2803 result[dp.ident] = haveTrans ? headToMri.map(posHead) : posHead;
2804 }
2805 return result;
2806}
2807
2808//=============================================================================================================
2809
2810bool BrainView::bemTopVertexInMri(QVector3D& pos) const
2811{
2812 auto it = m_surfaces.find(QStringLiteral("bem_head"));
2813 if (it == m_surfaces.end() || !it.value())
2814 return false;
2815
2816 const BrainSurface* surf = it.value().get();
2817 QVector3D bmin, bmax;
2818 surf->boundingBox(bmin, bmax);
2819
2820 // Top of head = max Z in MRI/surface-RAS (Z = superior)
2821 pos = QVector3D((bmin.x() + bmax.x()) * 0.5f,
2822 (bmin.y() + bmax.y()) * 0.5f,
2823 bmax.z());
2824 return true;
2825}
2826
2827//=============================================================================================================
2828
2829bool BrainView::loadMegHelmetSurface(const QString& helmetFilePath)
2830{
2831 auto surface = DataLoader::loadHelmetSurface(helmetFilePath, m_devHeadTrans, m_hasDevHead);
2832 if (!surface) {
2833 qWarning() << "BrainView::loadMegHelmetSurface: DataLoader returned nullptr!";
2834 return false;
2835 }
2836
2837 m_surfaces["sens_surface_meg"] = surface;
2838 refreshSensorTransforms();
2839 updateSceneBounds();
2840 m_sceneDirty = true;
2841 update();
2842 return true;
2843}
2844
2845//=============================================================================================================
2846
2847bool BrainView::loadDipoles(const QString& dipPath)
2848{
2849 if (!m_model)
2850 return false;
2851
2852 auto ecdSet = DataLoader::loadDipoles(dipPath);
2853 if (ecdSet.size() == 0)
2854 return false;
2855 m_model->addDipoles(ecdSet);
2856 return true;
2857}
2858
2859//=============================================================================================================
2860
2861bool BrainView::loadNetwork(const CONNECTIVITYLIB::Network& network, const QString& name)
2862{
2863 if (network.getNodes().isEmpty())
2864 return false;
2865
2866 m_network = std::make_unique<NetworkObject>();
2867 m_network->load(network);
2868 m_network->setVisible(true);
2869
2870 // Also register in the tree model
2871 if (m_model)
2872 m_model->addNetwork(network, name);
2873
2874 m_sceneDirty = true;
2875 update();
2876 return true;
2877}
2878
2879//=============================================================================================================
2880
2882{
2883 auto& profile = visibilityProfileForTarget(m_visualizationEditTarget);
2884 profile.network = visible;
2885 m_networkVisible = visible;
2886 if (m_network)
2887 m_network->setVisible(visible);
2888 saveMultiViewSettings();
2889 m_sceneDirty = true;
2890 update();
2891}
2892
2893//=============================================================================================================
2894
2896{
2897 if (m_network) {
2898 m_network->setThreshold(threshold);
2899 m_sceneDirty = true;
2900 update();
2901 }
2902}
2903
2904//=============================================================================================================
2905
2906void BrainView::setNetworkColormap(const QString& name)
2907{
2908 if (m_network) {
2909 m_network->setColormap(name);
2910 m_sceneDirty = true;
2911 update();
2912 }
2913}
2914
2915//=============================================================================================================
2916
2917bool BrainView::loadSourceSpace(const QString& fwdPath)
2918{
2919 if (!m_model)
2920 return false;
2921
2922 auto srcSpace = DataLoader::loadSourceSpace(fwdPath);
2923 if (srcSpace.isEmpty())
2924 return false;
2925 m_model->addSourceSpace(srcSpace);
2926 return true;
2927}
2928
2929//=============================================================================================================
2930
2932{
2933 auto& profile = visibilityProfileForTarget(m_visualizationEditTarget);
2934 profile.sourceSpace = visible;
2935 saveMultiViewSettings();
2936 m_sceneDirty = true;
2937 update();
2938}
2939
2940//=============================================================================================================
2941
2942bool BrainView::loadTransformation(const QString& transPath)
2943{
2944 FiffCoordTrans trans;
2945 if (!DataLoader::loadHeadToMriTransform(transPath, trans))
2946 return false;
2947
2948 m_headToMriTrans = trans;
2949 refreshSensorTransforms();
2950 return true;
2951}
2952
2953//==============================================================================
2954
2955QMatrix4x4 BrainView::itemTransform(const QStandardItem* item) const
2956{
2957 const auto* absItem = dynamic_cast<const AbstractTreeItem*>(item);
2958 if (!absItem)
2959 return {};
2960 QMatrix4x4 trans = absItem->transform();
2961 // Sensors, digitizer points and dipoles are in head coordinates
2962 const int type = absItem->type();
2964 if (inHead && m_applySensorTrans && !m_headToMriTrans.isEmpty())
2965 trans = SURFACEKEYS::toQMatrix4x4(m_headToMriTrans.trans) * trans;
2966 return trans;
2967}
2968
2969//==============================================================================
2970
2971void BrainView::refreshSensorTransforms()
2972{
2973 QMatrix4x4 qmat;
2974 if (m_applySensorTrans && !m_headToMriTrans.isEmpty()) {
2975 qmat = SURFACEKEYS::toQMatrix4x4(m_headToMriTrans.trans);
2976 }
2977
2978 for (auto it = m_surfaces.begin(); it != m_surfaces.end(); ++it) {
2979 if ((it.key().startsWith("sens_") || it.key().startsWith("dig_")) && it.value()) {
2980 const QStandardItem* item = m_itemSurfaceMap.key(it.value());
2981 it.value()->applyTransform(item ? itemTransform(item) : qmat);
2982 }
2983 }
2984 for (auto it = m_itemDipoleMap.cbegin(); it != m_itemDipoleMap.cend(); ++it) {
2985 it.value()->applyTransform(itemTransform(it.key()));
2986 }
2987
2988 if (m_fieldMapper.isLoaded()) {
2989 m_fieldMapper.buildMapping(m_surfaces, m_headToMriTrans, m_applySensorTrans);
2990 m_fieldMapper.apply(m_surfaces, m_singleView, m_subViews);
2991 }
2992}
2993
2994//=============================================================================================================
2995
2996void BrainView::castRay(const QPoint& pos)
2997{
2998 // 1. Setup Matrix Stack (Must match render exactly, including multiview pane layout)
2999 const QSize outputSize = size();
3000
3001 const auto enabledViewports = enabledViewportIndices();
3002
3003 const int numEnabled = enabledViewports.size();
3004 int activeSlot = 0;
3005 QRect activePane(0, 0, outputSize.width(), outputSize.height());
3006
3007 bool hasValidPane = true;
3008 if (m_viewMode == MultiView && numEnabled > 1) {
3009 bool foundSlot = false;
3010 for (int slot = 0; slot < numEnabled; ++slot) {
3011 const QRect pane = multiViewSlotRect(slot, numEnabled, outputSize);
3012 if (pane.contains(pos)) {
3013 activeSlot = slot;
3014 activePane = pane;
3015 foundSlot = true;
3016 break;
3017 }
3018 }
3019
3020 hasValidPane = foundSlot;
3021 }
3022
3023 const int vp = (m_viewMode == MultiView) ? enabledViewports[activeSlot] : 0;
3024 const SubView& sv = (m_viewMode == MultiView) ? m_subViews[vp] : m_singleView;
3025
3026 const QVector3D effectiveCenter2 = m_cameraFocusOverride ? m_cameraFocusCenter : m_sceneCenter;
3027 const float effectiveSize2 = m_cameraFocusOverride ? m_cameraFocusSize : m_sceneSize;
3028 m_camera.setSceneCenter(effectiveCenter2);
3029 m_camera.setSceneSize(effectiveSize2);
3030 m_camera.setRotation(m_cameraRotation);
3031 m_camera.setZoom(m_singleView.zoom);
3032 const float aspect = float(std::max(1, activePane.width())) / float(std::max(1, activePane.height()));
3033 const CameraResult cam = (m_viewMode == MultiView)
3034 ? m_camera.computeMultiView(sv, aspect)
3035 : m_camera.computeSingleView(aspect);
3036 QMatrix4x4 pvm = cam.projection * cam.view * cam.model;
3037
3038 // ── Unproject screen position to world-space ray ───────────────────
3039 QVector3D rayOrigin, rayDir;
3040 if (!RayPicker::unproject(pos, activePane, pvm, rayOrigin, rayDir))
3041 return;
3042
3043 // ── Pick against all scene geometry ────────────────────────────────
3044 RayHit pickResult;
3045 if (hasValidPane) {
3046 pickResult = RayPicker::pick(rayOrigin, rayDir, sv, m_surfaces, m_itemSurfaceMap, m_itemDipoleMap);
3047 }
3048 m_hasIntersection = pickResult.hit;
3049 if (pickResult.hit) {
3050 m_lastIntersectionPoint = pickResult.hitPoint;
3051 }
3052
3053 QStandardItem* hitItem = pickResult.item;
3054 int hitIndex = pickResult.vertexIndex;
3055
3056 // ── Build hover label ──────────────────────────────────────────────
3057 const QString displayLabel = RayPicker::buildLabel(pickResult, m_itemSurfaceMap, m_surfaces);
3058 const QString& hitKey = pickResult.surfaceKey;
3059 int currentRegionId = pickResult.regionId;
3060
3061 if (displayLabel != m_hoveredRegion) {
3062 m_hoveredRegion = displayLabel;
3063 emit hoveredRegionChanged(m_hoveredRegion);
3064 if (m_regionLabel) {
3065 if (m_hoveredRegion.isEmpty()) {
3066 m_regionLabel->hide();
3067 } else {
3068 m_regionLabel->setText(m_hoveredRegion);
3069 m_regionLabel->show();
3070 }
3071 }
3072 }
3073
3074 QString hoveredSurfaceKey;
3075 if (hitKey.startsWith("sens_surface_meg")) {
3076 hoveredSurfaceKey = hitKey;
3077 }
3078
3079 if (hitItem != m_hoveredItem || hitIndex != m_hoveredIndex || hoveredSurfaceKey != m_hoveredSurfaceKey) {
3080 // Deselect previous
3081 if (m_hoveredItem) {
3082 if (m_itemSurfaceMap.contains(m_hoveredItem)) {
3083 m_itemSurfaceMap[m_hoveredItem]->setSelected(false);
3084 m_itemSurfaceMap[m_hoveredItem]->setSelectedRegion(-1);
3085 m_itemSurfaceMap[m_hoveredItem]->setSelectedVertexRange(-1, 0);
3086 } else if (m_itemDipoleMap.contains(m_hoveredItem)) {
3087 m_itemDipoleMap[m_hoveredItem]->setSelected(m_hoveredIndex, false);
3088 }
3089 }
3090 if (!m_hoveredSurfaceKey.isEmpty() && m_surfaces.contains(m_hoveredSurfaceKey)) {
3091 m_surfaces[m_hoveredSurfaceKey]->setSelected(false);
3092 m_surfaces[m_hoveredSurfaceKey]->setSelectedRegion(-1);
3093 m_surfaces[m_hoveredSurfaceKey]->setSelectedVertexRange(-1, 0);
3094 }
3095
3096 m_hoveredItem = hitItem;
3097 m_hoveredIndex = hitIndex;
3098 m_hoveredSurfaceKey = hoveredSurfaceKey;
3099
3100 if (m_hoveredItem) {
3101 // Select new
3102 if (m_itemSurfaceMap.contains(m_hoveredItem)) {
3103 // Check if this is a digitizer batched mesh — highlight single sphere
3104 AbstractTreeItem* absHitSel = dynamic_cast<AbstractTreeItem*>(m_hoveredItem);
3105 bool isDigitizer = absHitSel &&
3107
3108 if (isDigitizer && m_hoveredIndex >= 0) {
3109 const int vertsPerSphere = MeshFactory::sphereVertexCount();
3110 int sphereIdx = m_hoveredIndex / vertsPerSphere;
3111 m_itemSurfaceMap[m_hoveredItem]->setSelectedVertexRange(
3112 sphereIdx * vertsPerSphere, vertsPerSphere);
3113 } else if (currentRegionId != -1) {
3114 m_itemSurfaceMap[m_hoveredItem]->setSelectedRegion(currentRegionId);
3115 // Keep the surface selected so the shader gold glow
3116 // activates. The old CPU vertex-color region highlight
3117 // was removed to avoid buffer re-uploads on WASM.
3118 m_itemSurfaceMap[m_hoveredItem]->setSelected(true);
3119 } else {
3120 m_itemSurfaceMap[m_hoveredItem]->setSelected(true);
3121 m_itemSurfaceMap[m_hoveredItem]->setSelectedRegion(-1);
3122 }
3123 } else if (m_itemDipoleMap.contains(m_hoveredItem)) {
3124 m_itemDipoleMap[m_hoveredItem]->setSelected(m_hoveredIndex, true);
3125 }
3126 } else if (!m_hoveredSurfaceKey.isEmpty() && m_surfaces.contains(m_hoveredSurfaceKey)) {
3127 m_surfaces[m_hoveredSurfaceKey]->setSelected(true);
3128 m_surfaces[m_hoveredSurfaceKey]->setSelectedRegion(-1);
3129 }
3130 } else if (m_hoveredItem && m_itemSurfaceMap.contains(m_hoveredItem)) {
3131 AbstractTreeItem* absHitUpd = dynamic_cast<AbstractTreeItem*>(m_hoveredItem);
3132 bool isDigitizer = absHitUpd &&
3134
3135 if (isDigitizer && m_hoveredIndex >= 0) {
3136 const int vertsPerSphere = MeshFactory::sphereVertexCount();
3137 int sphereIdx = m_hoveredIndex / vertsPerSphere;
3138 m_itemSurfaceMap[m_hoveredItem]->setSelectedVertexRange(
3139 sphereIdx * vertsPerSphere, vertsPerSphere);
3140 } else if (currentRegionId != -1) {
3141 m_itemSurfaceMap[m_hoveredItem]->setSelectedRegion(currentRegionId);
3142 m_itemSurfaceMap[m_hoveredItem]->setSelected(true);
3143 } else {
3144 m_itemSurfaceMap[m_hoveredItem]->setSelectedRegion(-1);
3145 m_itemSurfaceMap[m_hoveredItem]->setSelected(true);
3146 }
3147 } else if (!m_hoveredSurfaceKey.isEmpty() && m_surfaces.contains(m_hoveredSurfaceKey)) {
3148 m_surfaces[m_hoveredSurfaceKey]->setSelected(true);
3149 }
3150 m_sceneDirty = true;
3151 update();
3152}
3153
3154//=============================================================================================================
3155
3156void BrainView::showViewportPresetMenu(int viewport, const QPoint& globalPos)
3157{
3158 if (viewport < 0 || viewport >= m_subViews.size()) {
3159 return;
3160 }
3161
3162 QMenu menu;
3163 QAction* topAction = menu.addAction("Top");
3164 QAction* perspectiveAction = menu.addAction("Perspective");
3165 QAction* frontAction = menu.addAction("Front");
3166 QAction* leftAction = menu.addAction("Left");
3167 menu.addSeparator();
3168 QAction* bottomAction = menu.addAction("Bottom");
3169 QAction* backAction = menu.addAction("Back");
3170 QAction* rightAction = menu.addAction("Right");
3171
3172 const int currentPreset = std::clamp(m_subViews[viewport].preset, 0, 6);
3173 topAction->setCheckable(true);
3174 perspectiveAction->setCheckable(true);
3175 frontAction->setCheckable(true);
3176 leftAction->setCheckable(true);
3177 bottomAction->setCheckable(true);
3178 backAction->setCheckable(true);
3179 rightAction->setCheckable(true);
3180
3181 topAction->setChecked(currentPreset == 0);
3182 perspectiveAction->setChecked(currentPreset == 1);
3183 frontAction->setChecked(currentPreset == 2);
3184 leftAction->setChecked(currentPreset == 3);
3185 bottomAction->setChecked(currentPreset == 4);
3186 backAction->setChecked(currentPreset == 5);
3187 rightAction->setChecked(currentPreset == 6);
3188
3189 QAction* selected = menu.exec(globalPos);
3190 if (!selected) {
3191 return;
3192 }
3193
3194 int newPreset = currentPreset;
3195 if (selected == topAction) {
3196 newPreset = 0;
3197 } else if (selected == perspectiveAction) {
3198 newPreset = 1;
3199 } else if (selected == frontAction) {
3200 newPreset = 2;
3201 } else if (selected == leftAction) {
3202 newPreset = 3;
3203 } else if (selected == bottomAction) {
3204 newPreset = 4;
3205 } else if (selected == backAction) {
3206 newPreset = 5;
3207 } else if (selected == rightAction) {
3208 newPreset = 6;
3209 }
3210
3211 if (newPreset == currentPreset) {
3212 return;
3213 }
3214
3215 m_subViews[viewport].preset = newPreset;
3216 saveMultiViewSettings();
3217 updateOverlayLayout();
3218 m_sceneDirty = true;
3219 update();
3220}
3221
3222//=============================================================================================================
3223// Data removal
3224//=============================================================================================================
3225
3226void BrainView::removeSurfacesByPrefix(const QStringList& prefixes)
3227{
3228 // Remove the tree rows first: they are found through the surfaces being removed
3229 for (auto it = m_itemSurfaceMap.begin(); it != m_itemSurfaceMap.end();) {
3230 const QString key = m_surfaces.key(it.value());
3231 const bool remove = std::any_of(prefixes.cbegin(), prefixes.cend(), [&key](const QString& prefix) { return key.startsWith(prefix); });
3232 if (!remove) {
3233 ++it;
3234 continue;
3235 }
3236 if (m_model) {
3237 QStandardItem* item = const_cast<QStandardItem*>(it.key());
3238 if (item->parent())
3239 item->parent()->removeRow(item->row());
3240 else
3241 m_model->removeRow(item->row());
3242 }
3243 it = m_itemSurfaceMap.erase(it);
3244 }
3245 for (auto it = m_surfaces.begin(); it != m_surfaces.end();) {
3246 const QString& key = it.key();
3247 if (std::any_of(prefixes.cbegin(), prefixes.cend(), [&key](const QString& prefix) { return key.startsWith(prefix); }))
3248 it = m_surfaces.erase(it);
3249 else
3250 ++it;
3251 }
3252 updateSceneBounds();
3253 m_sceneDirty = true;
3254 update();
3255}
3256
3257//=============================================================================================================
3258
3260{
3261 m_activeSurface.reset();
3262 m_activeSurfaceType.clear();
3263 removeSurfacesByPrefix({QStringLiteral("lh_"), QStringLiteral("rh_")});
3264}
3265
3266//=============================================================================================================
3267
3269{
3270 removeSurfacesByPrefix({QStringLiteral("bem_")});
3271}
3272
3273//=============================================================================================================
3274
3276{
3277 m_sourceManager.stopStreaming();
3278 for (auto it = m_surfaces.begin(); it != m_surfaces.end(); ++it) {
3279 if (it.key().startsWith("lh_") || it.key().startsWith("rh_")) {
3280 it.value()->clearSourceEstimateColors();
3281 }
3282 }
3283 m_sceneDirty = true;
3284 update();
3285}
3286
3287//=============================================================================================================
3288
3290{
3291 m_dipoles.reset();
3292
3293 // Remove dipole items from model and maps
3294 for (auto it = m_itemDipoleMap.begin(); it != m_itemDipoleMap.end();) {
3295 if (m_model) {
3296 QStandardItem* mutableItem = const_cast<QStandardItem*>(it.key());
3297 if (mutableItem->parent())
3298 mutableItem->parent()->removeRow(mutableItem->row());
3299 else
3300 m_model->removeRow(mutableItem->row());
3301 }
3302 it = m_itemDipoleMap.erase(it);
3303 }
3304 m_sceneDirty = true;
3305 update();
3306}
3307
3308//=============================================================================================================
3309
3311{
3312 removeSurfacesByPrefix({QStringLiteral("srcsp_")});
3313}
3314
3315//=============================================================================================================
3316
3317void BrainView::setLiveMarkers(const QVector<LiveMarker>& markers)
3318{
3319 // Remove previous live surfaces (no scene-bounds update).
3320 for (auto it = m_surfaces.begin(); it != m_surfaces.end();) {
3321 if (it.key().startsWith(QLatin1String("dig_live_")))
3322 it = m_surfaces.erase(it);
3323 else
3324 ++it;
3325 }
3326
3327 for (int i = 0; i < markers.size(); ++i) {
3329 {markers[i].position}, markers[i].radius, markers[i].color);
3330 surf->setVisible(true);
3331 const QString prefix = markers[i].transparent
3332 ? QStringLiteral("dig_live_t_%1")
3333 : QStringLiteral("dig_live_%1");
3334 m_surfaces[prefix.arg(i)] = surf;
3335 }
3336
3337 // Intentionally NO updateSceneBounds() — the camera stays locked.
3338 m_sceneDirty = true;
3339 update();
3340}
3341
3343{
3344 bool removed = false;
3345 for (auto it = m_surfaces.begin(); it != m_surfaces.end();) {
3346 if (it.key().startsWith(QLatin1String("dig_live_"))) {
3347 it = m_surfaces.erase(it);
3348 removed = true;
3349 } else {
3350 ++it;
3351 }
3352 }
3353 if (removed) {
3354 m_sceneDirty = true;
3355 update();
3356 }
3357}
3358
3359void BrainView::setLiveRay(const QVector3D& from, const QVector3D& to,
3360 const QColor& color, float radius)
3361{
3362 // Remove previous ray surface
3363 m_surfaces.remove(QLatin1String("dig_ray_0"));
3364
3365 auto surf = MeshFactory::createCylinder(from, to, radius, color);
3366 surf->setVisible(true);
3367 m_surfaces[QStringLiteral("dig_ray_0")] = surf;
3368
3369 m_sceneDirty = true;
3370 update();
3371}
3372
3374{
3375 if (m_surfaces.remove(QLatin1String("dig_ray_0"))) {
3376 m_sceneDirty = true;
3377 update();
3378 }
3379}
3380
3381void BrainView::setProbeVisualization(const QVector3D& tip, const QVector3D& direction,
3382 float length, const QColor& color,
3383 const QColor& glowColor,
3384 const QQuaternion& orientation)
3385{
3386 // Remove previous probe surfaces
3387 m_surfaces.remove(QLatin1String("dig_probe_shaft"));
3388 m_surfaces.remove(QLatin1String("dig_probe_tip"));
3389 m_surfaces.remove(QLatin1String("dig_probe_tipglow"));
3390 m_surfaces.remove(QLatin1String("dig_probe_axis_x"));
3391 m_surfaces.remove(QLatin1String("dig_probe_axis_y"));
3392 m_surfaces.remove(QLatin1String("dig_probe_axis_z"));
3393 m_surfaces.remove(QLatin1String("dig_probe_axis_x_tip"));
3394 m_surfaces.remove(QLatin1String("dig_probe_axis_y_tip"));
3395 m_surfaces.remove(QLatin1String("dig_probe_axis_z_tip"));
3396 m_surfaces.remove(QLatin1String("dig_probe_axis_xn_tip"));
3397 m_surfaces.remove(QLatin1String("dig_probe_axis_yn_tip"));
3398 m_surfaces.remove(QLatin1String("dig_probe_axis_zn_tip"));
3399 m_surfaces.remove(QLatin1String("dig_probe_cross_x"));
3400 m_surfaces.remove(QLatin1String("dig_probe_cross_y"));
3401 m_surfaces.remove(QLatin1String("dig_probe_cross_z"));
3402
3403 // Shaft: only drawn when length > 0
3404 if (length > 0.0f && !direction.isNull()) {
3405 const QVector3D shaftEnd = tip - direction * length;
3406 constexpr float kShaftRadius = 0.0015f;
3407 auto shaft = MeshFactory::createCylinder(tip, shaftEnd, kShaftRadius, color);
3408 shaft->setVisible(true);
3409 m_surfaces[QStringLiteral("dig_probe_shaft")] = shaft;
3410 }
3411
3412 // Tip: the focal point of the probe
3413 constexpr float kTipRadius = 0.0010f; // 1.0 mm
3414 auto tipSurf = MeshFactory::createBatchedSpheres({tip}, kTipRadius, color);
3415 tipSurf->setVisible(true);
3416 m_surfaces[QStringLiteral("dig_probe_tip")] = tipSurf;
3417
3418 // Glow aura: subtle halo around the tip
3419 if (glowColor.alpha() > 0) {
3420 constexpr float kGlowTipRadius = 0.003f; // 3 mm glow
3421 auto tipGlow = MeshFactory::createBatchedSpheres({tip}, kGlowTipRadius, glowColor);
3422 tipGlow->setVisible(true);
3423 m_surfaces[QStringLiteral("dig_probe_tipglow")] = tipGlow;
3424 }
3425
3426 // --- Crosshair rotated with probe orientation (X=red, Y=green, Z=blue) ---
3427 if (!orientation.isNull()) {
3428 constexpr float kCrossLen = 0.008f; // 8 mm per arm
3429 constexpr float kCrossRadius = 0.0002f; // 0.2 mm — hair-thin
3430
3431 const QVector3D xDir = orientation.rotatedVector(QVector3D(1, 0, 0)).normalized();
3432 const QVector3D yDir = orientation.rotatedVector(QVector3D(0, 1, 0)).normalized();
3433 const QVector3D zDir = orientation.rotatedVector(QVector3D(0, 0, 1)).normalized();
3434
3435 struct CrossDef
3436 {
3437 QVector3D dir;
3438 QColor color;
3439 QString key;
3440 };
3441 const CrossDef cross[] = {
3442 {xDir, QColor(255, 50, 50, 160), QStringLiteral("dig_probe_cross_x")},
3443 {yDir, QColor(50, 220, 50, 160), QStringLiteral("dig_probe_cross_y")},
3444 {zDir, QColor(80, 140, 255, 160), QStringLiteral("dig_probe_cross_z")},
3445 };
3446 for (const auto& c : cross) {
3447 const QVector3D from = tip - c.dir * kCrossLen;
3448 const QVector3D to = tip + c.dir * kCrossLen;
3449 auto cyl = MeshFactory::createCylinder(from, to, kCrossRadius, c.color);
3450 cyl->setVisible(true);
3451 m_surfaces[c.key] = cyl;
3452 }
3453 }
3454
3455 // --- Debug coordinate frame (X=red, Y=green, Z=blue) ---
3456 if (!orientation.isNull()) {
3457 constexpr float kAxisLen = 0.012f; // 12 mm per axis arm
3458 constexpr float kAxisRadius = 0.0004f; // 0.4 mm — thin axis lines
3459 constexpr float kPosTipR = 0.0012f; // 1.2 mm — positive-end sphere
3460 constexpr float kNegTipR = 0.0006f; // 0.6 mm — negative-end dot (smaller)
3461
3462 const QVector3D xDir = orientation.rotatedVector(QVector3D(1, 0, 0)).normalized();
3463 const QVector3D yDir = orientation.rotatedVector(QVector3D(0, 1, 0)).normalized();
3464 const QVector3D zDir = orientation.rotatedVector(QVector3D(0, 0, 1)).normalized();
3465
3466 struct AxisDef
3467 {
3468 QVector3D dir;
3469 QColor color;
3470 QString cylKey, posTipKey, negTipKey;
3471 };
3472 const AxisDef axes[] = {
3473 {xDir, QColor(255, 50, 50), QStringLiteral("dig_probe_axis_x"),
3474 QStringLiteral("dig_probe_axis_x_tip"), QStringLiteral("dig_probe_axis_xn_tip")},
3475 {yDir, QColor(50, 220, 50), QStringLiteral("dig_probe_axis_y"),
3476 QStringLiteral("dig_probe_axis_y_tip"), QStringLiteral("dig_probe_axis_yn_tip")},
3477 {zDir, QColor(80, 140, 255), QStringLiteral("dig_probe_axis_z"),
3478 QStringLiteral("dig_probe_axis_z_tip"), QStringLiteral("dig_probe_axis_zn_tip")},
3479 };
3480
3481 for (const auto& a : axes) {
3482 const QVector3D posEnd = tip + a.dir * kAxisLen;
3483 const QVector3D negEnd = tip - a.dir * kAxisLen;
3484
3485 // Full axis cylinder from -axis to +axis through the tip
3486 auto cyl = MeshFactory::createCylinder(negEnd, posEnd, kAxisRadius, a.color);
3487 cyl->setVisible(true);
3488 m_surfaces[a.cylKey] = cyl;
3489
3490 // Positive tip: larger sphere (the "+" end)
3491 auto posTip = MeshFactory::createBatchedSpheres({posEnd}, kPosTipR, a.color);
3492 posTip->setVisible(true);
3493 m_surfaces[a.posTipKey] = posTip;
3494
3495 // Negative tip: smaller dot (the "-" end)
3496 QColor dimColor = a.color;
3497 dimColor.setAlpha(120);
3498 auto negTip = MeshFactory::createBatchedSpheres({negEnd}, kNegTipR, dimColor);
3499 negTip->setVisible(true);
3500 m_surfaces[a.negTipKey] = negTip;
3501 }
3502 }
3503
3504 m_sceneDirty = true;
3505 update();
3506}
3507
3509{
3510 // QMap::remove returns the number of entries removed, not a bool, so sum
3511 // the counts rather than OR-ing an integer into a bool.
3512 qsizetype nRemoved = 0;
3513 for (const char* key : {"dig_probe_shaft",
3514 "dig_probe_tip",
3515 "dig_probe_tipglow",
3516 "dig_probe_axis_x",
3517 "dig_probe_axis_y",
3518 "dig_probe_axis_z",
3519 "dig_probe_axis_x_tip",
3520 "dig_probe_axis_y_tip",
3521 "dig_probe_axis_z_tip",
3522 "dig_probe_axis_xn_tip",
3523 "dig_probe_axis_yn_tip",
3524 "dig_probe_axis_zn_tip",
3525 "dig_probe_cross_x",
3526 "dig_probe_cross_y",
3527 "dig_probe_cross_z"}) {
3528 nRemoved += m_surfaces.remove(QLatin1String(key));
3529 }
3530
3531 const bool removed = nRemoved > 0;
3532 if (removed) {
3533 m_sceneDirty = true;
3534 update();
3535 }
3536}
3537
3538void BrainView::setStaticMarkers(const QVector<LiveMarker>& markers)
3539{
3540 for (auto it = m_surfaces.begin(); it != m_surfaces.end();) {
3541 if (it.key().startsWith(QLatin1String("dig_static_")))
3542 it = m_surfaces.erase(it);
3543 else
3544 ++it;
3545 }
3546
3547 for (int i = 0; i < markers.size(); ++i) {
3549 {markers[i].position}, markers[i].radius, markers[i].color);
3550 surf->setVisible(true);
3551 m_surfaces[QStringLiteral("dig_static_%1").arg(i)] = surf;
3552 }
3553
3554 m_sceneDirty = true;
3555 update();
3556}
3557
3559{
3560 bool removed = false;
3561 for (auto it = m_surfaces.begin(); it != m_surfaces.end();) {
3562 if (it.key().startsWith(QLatin1String("dig_static_"))) {
3563 it = m_surfaces.erase(it);
3564 removed = true;
3565 } else {
3566 ++it;
3567 }
3568 }
3569 if (removed) {
3570 m_sceneDirty = true;
3571 update();
3572 }
3573}
3574
3575void BrainView::setCameraFocusOverride(const QVector3D& center, float size)
3576{
3577 m_cameraFocusOverride = true;
3578 m_cameraFocusCenter = center;
3579 m_cameraFocusSize = size;
3580 m_sceneDirty = true;
3581 update();
3582}
3583
3585{
3586 if (!m_cameraFocusOverride)
3587 return;
3588 m_cameraFocusOverride = false;
3589 updateSceneBounds();
3590 m_sceneDirty = true;
3591 update();
3592}
3593
3595{
3596 m_devHeadTrans = QMatrix4x4();
3597 m_hasDevHead = false;
3598 removeSurfacesByPrefix({QStringLiteral("sens_"), QStringLiteral("dig_")});
3599}
3600
3601//=============================================================================================================
3602
3604{
3605 m_fieldMapper.setEvoked(FIFFLIB::FiffEvoked());
3606 m_sensorStreamManager.stopStreaming();
3607 m_sceneDirty = true;
3608 update();
3609}
3610
3611//=============================================================================================================
3612
3614{
3615 m_headToMriTrans = FIFFLIB::FiffCoordTrans();
3616 refreshSensorTransforms();
3617 m_sceneDirty = true;
3618 update();
3619}
3620
3621//=============================================================================================================
3622
3624{
3625 m_network.reset();
3626 m_networkVisible = false;
3627
3628 // Remove network items from model
3629 if (m_model) {
3630 for (int r = m_model->rowCount() - 1; r >= 0; --r) {
3631 QStandardItem* item = m_model->item(r);
3632 if (item && item->text() == "Networks") {
3633 m_model->removeRow(r);
3634 break;
3635 }
3636 }
3637 }
3638 m_sceneDirty = true;
3639 update();
3640}
3641
3642//=============================================================================================================
3643
3645{
3646 m_sourceManager.cancelLoading();
3647 m_sourceManager.stopStreaming();
3648 m_sensorStreamManager.stopStreaming();
3649}
3650
3651//=============================================================================================================
3652// Video overlay public API
3653//=============================================================================================================
3654
3656{
3657 if (!m_videoOverlay)
3658 return;
3659 if (m_videoOverlay->isEnabled() == enabled)
3660 return;
3661 m_videoOverlay->setEnabled(enabled);
3662 m_sceneDirty = true;
3663 update();
3664}
3665
3667{
3668 return m_videoOverlay && m_videoOverlay->isEnabled();
3669}
3670
3671void BrainView::setVideoOverlayFocusPosition(const QVector3D& position)
3672{
3673 if (!m_videoOverlay)
3674 return;
3675 m_videoOverlay->setFocusPosition(position);
3676 if (m_videoOverlay->isEnabled()) {
3677 m_sceneDirty = true;
3678 update();
3679 }
3680}
3681
3682void BrainView::setVideoOverlayUpHint(const QVector3D& dir)
3683{
3684 if (!m_videoOverlay)
3685 return;
3686 m_videoOverlay->setUpHint(dir);
3687 if (m_videoOverlay->isEnabled()) {
3688 m_sceneDirty = true;
3689 update();
3690 }
3691}
3692
3694{
3695 if (!m_videoOverlay)
3696 return;
3697 m_videoOverlay->setSizeMeters(std::max(0.001f, meters));
3698 if (m_videoOverlay->isEnabled()) {
3699 m_sceneDirty = true;
3700 update();
3701 }
3702}
3703
3705{
3706 if (!m_videoOverlay)
3707 return;
3708 m_videoOverlay->setOpacity(std::clamp(opacity, 0.0f, 1.0f));
3709 if (m_videoOverlay->isEnabled()) {
3710 m_sceneDirty = true;
3711 update();
3712 }
3713}
3714
3715void BrainView::pushVideoOverlayFrame(const QImage& frame)
3716{
3717 if (!m_videoOverlay)
3718 return;
3719 m_videoOverlay->setFrame(frame);
3720 if (m_videoOverlay->isEnabled()) {
3721 m_sceneDirty = true;
3722 update();
3723 }
3724}
3725
3727{
3728 if (!m_videoOverlay)
3729 return;
3730 m_videoOverlay->setDepthEnabled(enabled);
3731 if (m_videoOverlay->isEnabled()) {
3732 m_sceneDirty = true;
3733 update();
3734 }
3735}
3736
3738{
3739 if (!m_videoOverlay)
3740 return;
3741 m_videoOverlay->setDepthScale(std::clamp(scale, 0.0f, 1.0f));
3742 if (m_videoOverlay->isEnabled()) {
3743 m_sceneDirty = true;
3744 update();
3745 }
3746}
3747
3749{
3750 if (!m_videoOverlay)
3751 return;
3752 m_videoOverlay->setDepthSteps(std::clamp(steps, 8, 64));
3753 if (m_videoOverlay->isEnabled()) {
3754 m_sceneDirty = true;
3755 update();
3756 }
3757}
3758
3759void BrainView::pushVideoDepthFrame(const QImage& depthFrame)
3760{
3761 if (!m_videoOverlay)
3762 return;
3763 m_videoOverlay->setDepthFrame(depthFrame);
3764 if (m_videoOverlay->isEnabled() && m_videoOverlay->isDepthEnabled()) {
3765 m_sceneDirty = true;
3766 update();
3767 }
3768}
3769
3770bool BrainView::intersectWorldRay(const QVector3D& origin, const QVector3D& direction, QVector3D& hitPoint) const
3771{
3772 // Test against ALL surfaces (no SubView visibility filter) so
3773 // the optical ray always finds the head even if it's hidden in
3774 // the first SubView of a multi-view layout.
3775 float closestDist = std::numeric_limits<float>::max();
3776 bool found = false;
3777
3778 // Always test the BEM head surface first (even when hidden) — the
3779 // optical projection cares about the outermost physical shell.
3780 auto headIt = m_surfaces.constFind(QStringLiteral("bem_head"));
3781 if (headIt != m_surfaces.cend() && headIt.value()) {
3782 float dist = 0.0f;
3783 int vertexIdx = -1;
3784 if (headIt.value()->intersects(origin, direction, dist, vertexIdx) && dist < closestDist) {
3785 closestDist = dist;
3786 hitPoint = origin + dist * direction;
3787 found = true;
3788 }
3789 }
3790
3791 // Fall back to any other visible surface if the head wasn't hit.
3792 if (!found) {
3793 for (auto it = m_surfaces.cbegin(); it != m_surfaces.cend(); ++it) {
3794 const auto& surf = it.value();
3795 if (!surf || !surf->isVisible())
3796 continue;
3797 float dist = 0.0f;
3798 int vertexIdx = -1;
3799 if (surf->intersects(origin, direction, dist, vertexIdx) && dist < closestDist) {
3800 closestDist = dist;
3801 hitPoint = origin + dist * direction;
3802 found = true;
3803 }
3804 }
3805 }
3806 return found;
3807}
3808
3809//=============================================================================================================
3810// MRI slice rendering
3811//=============================================================================================================
3812
3814{
3815 if (slotIndex < 0 || slotIndex >= kMaxSliceSlots)
3816 return;
3817 m_slices[slotIndex] = slice;
3818 m_sceneDirty = true;
3819 update();
3820}
3821
3822void BrainView::setSliceVisible(int slotIndex, bool visible)
3823{
3824 if (slotIndex < 0 || slotIndex >= kMaxSliceSlots)
3825 return;
3826 m_sliceVisible[slotIndex] = visible;
3827 m_sceneDirty = true;
3828 update();
3829}
3830
3831//=============================================================================================================
3832
3834{
3835 auto& profile = visibilityProfileForTarget(m_visualizationEditTarget);
3836 profile.mriSlices = visible;
3837 saveMultiViewSettings();
3838 m_sceneDirty = true;
3839 update();
3840}
3841
3842} // namespace DISP3DLIB
Symbolic FIFF tag, block, value, unit and channel-type constants shared across FIFFLIB.
#define FIFFV_POINT_CARDINAL
Set of averaged evoked responses sharing a FiffInfo, plus the ave-style category / rejection descript...
QStandardItemModel hierarchy that organises every 3-D scene object (surfaces, sensors,...
Tree item holding a single category of digitizer points rendered as a batched-sphere mesh.
Tree item wrapping a fitted INVLIB::InvEcdSet of equivalent current dipoles.
Tree item wrapping a single MNELIB::MNEBemSurface (brain / inner-skull / outer-skull / scalp shell).
Tree item for a single MEG / EEG sensor with position, optional coil orientation and rendered scale.
Tree item wrapping a FreeSurfer FSLIB::FsSurface plus optional FSLIB::FsAnnotation parcellation.
Tree item holding one hemisphere of source-space dipole positions rendered as batched spheres.
Mouse-ray vs scene-object intersection for picking dipoles, electrodes and surfaces.
Static factory for procedural Qt-RHI meshes (spheres, plates, barbells, cylinders,...
Top-level QRhiWidget that hosts the 3-D scene, drives camera interaction and exposes the multi-view +...
Qt-RHI scene renderer: shader pipelines, lighting, dual render targets and per-frame draw orchestrati...
Instanced connectivity-graph renderable: node spheres and edge cylinders coloured by weight through a...
PolylineObject class declaration.
Instanced-arrow renderable for fitted equivalent current dipoles, driven by QRhi instancing.
Renderable cortical / BEM mesh with interleaved vertex attributes and Qt-RHI buffer management.
Single MRI volume slice rendered as a textured quad with adjustable axis, position,...
Generic live-RGB video texture overlay rendered as a screen-aligned quad with chroma keying.
String key constants for surfaces in the DISP3DLIB scene map.
Static helpers for loading FreeSurfer / MNE source-space and BEM data into the disp3D model tree.
Node of a connectivity CONNECTIVITYLIB::Network; carries a 3D position and the lists of incident (in ...
Weighted edge between two CONNECTIVITYLIB::NetworkNode instances; stores the full per-frequency weigh...
Graph container that stores the result of one functional-connectivity metric as nodes (sources/sensor...
Boundary element model bundle (inner skull, outer skull, outer skin) loaded from -bem....
Container pairing the left and right cortical source spaces of a subject.
FIFF file I/O, in-memory data structures and high-level readers/writers.
3-D brain visualisation using the Qt RHI rendering backend.
ShaderMode shaderModeFromName(const QString &name)
QString shaderModeName(ShaderMode mode)
QString visualizationModeName(VisualizationMode mode)
bool multiViewPresetIsPerspective(int preset)
QString multiViewPresetName(int preset)
int normalizedVisualizationTarget(int target, int maxIndex)
VisualizationMode visualizationModeFromName(const QString &name)
QString sensorParentToKeyPrefix(const QString &parentText)
QString sensorTypeToObjectKey(const QString &uiType)
Definition surfacekeys.h:97
QMatrix4x4 toQMatrix4x4(const Eigen::Matrix4f &m)
Graph container for one connectivity metric; nodes + weighted edges + threshold/visualisation state.
Definition network.h:106
const QList< QSharedPointer< NetworkNode > > & getNodes() const
Definition network.cpp:136
static std::shared_ptr< BrainSurface > loadHelmetSurface(const QString &helmetFilePath, const QMatrix4x4 &devHeadTrans=QMatrix4x4(), bool applyTrans=false)
static FIFFLIB::FiffEvoked loadEvoked(const QString &evokedPath, int aveIndex=0)
static MNELIB::MNESourceSpaces loadSourceSpace(const QString &fwdPath)
static INVLIB::InvEcdSet loadDipoles(const QString &dipPath)
static QStringList probeEvokedSets(const QString &evokedPath)
static SensorLoadResult loadSensors(const QString &fifPath, const QString &megHelmetOverridePath={})
static bool loadHeadToMriTransform(const QString &transPath, FIFFLIB::FiffCoordTrans &trans)
Per-view toggle flags controlling which data layers (brain, sensors, sources, network) are visible.
Definition viewstate.h:68
Viewport subdivision holding its own camera, projection, and scissor rectangle.
Definition viewstate.h:143
ShaderMode brainShader
Definition viewstate.h:146
static SubView defaultForIndex(int index)
bool matchesSurfaceType(const QString &key) const
bool shouldRenderSurface(const QString &key) const
ShaderMode bemShader
Definition viewstate.h:147
ViewVisibilityProfile visibility
Definition viewstate.h:149
VisualizationMode overlayMode
Definition viewstate.h:148
static bool isBrainSurfaceKey(const QString &key)
static std::shared_ptr< BrainSurface > createBarbell(const QVector3D &center, const QMatrix4x4 &orientation, const QColor &color, float size)
static std::shared_ptr< BrainSurface > createCylinder(const QVector3D &from, const QVector3D &to, float radius, const QColor &color, int sides=12)
static std::shared_ptr< BrainSurface > createBatchedSpheres(const QVector< QVector3D > &positions, float radius, const QColor &color, int subdivisions=1)
static std::shared_ptr< BrainSurface > createSphere(const QVector3D &center, float radius, const QColor &color, int subdivisions=1)
static int sphereVertexCount(int subdivisions=1)
static std::shared_ptr< BrainSurface > createPlate(const QVector3D &center, const QMatrix4x4 &orientation, const QColor &color, float size)
Computed camera matrices (projection, view, model) and vectors for a single viewport.
static void applyMousePan(const QPoint &delta, QVector2D &pan, float sceneSize)
static void applyMouseRotation(const QPoint &delta, QQuaternion &rotation, float speed=0.5f)
Result of a ray–mesh intersection test containing the hit point, triangle index, and distance.
Definition raypicker.h:58
QString surfaceKey
FsSurface map key of the hit surface.
Definition raypicker.h:64
int regionId
FsAnnotation label ID.
Definition raypicker.h:72
QStandardItem * item
Tree item that was hit (nullable).
Definition raypicker.h:63
bool hit
True if something was hit.
Definition raypicker.h:59
QVector3D hitPoint
World-space intersection point.
Definition raypicker.h:61
int vertexIndex
Vertex or element index at hit.
Definition raypicker.h:65
static bool unproject(const QPoint &screenPos, const QRect &paneRect, const QMatrix4x4 &pvm, QVector3D &rayOrigin, QVector3D &rayDir)
Definition raypicker.cpp:33
static RayHit pick(const QVector3D &rayOrigin, const QVector3D &rayDir, const SubView &subView, const QMap< QString, std::shared_ptr< BrainSurface > > &surfaces, const QMap< const QStandardItem *, std::shared_ptr< BrainSurface > > &itemSurfaceMap, const QMap< const QStandardItem *, std::shared_ptr< DipoleObject > > &itemDipoleMap)
Definition raypicker.cpp:67
static QString buildLabel(const RayHit &result, const QMap< const QStandardItem *, std::shared_ptr< BrainSurface > > &itemSurfaceMap, const QMap< QString, std::shared_ptr< BrainSurface > > &surfaces)
Hierarchical item model organizing all 3-D scene objects (surfaces, sensors, sources,...
Base tree item providing check-state, visibility, and data-role storage for all 3-D scene items.
static constexpr int itemTypeId(ItemType type)
Tree item representing a BEM surface layer in the 3-D scene hierarchy.
Definition bemtreeitem.h:38
const MNELIB::MNEBemSurface & bemSurfaceData() const
Digitizer point group tree item.
const QVector< QVector3D > & positions() const
Tree item representing a set of fitted dipoles in the 3-D scene hierarchy.
const INVLIB::InvEcdSet & ecdSet() const
Tree item representing MEG or EEG sensor positions in the 3-D scene hierarchy.
const QMatrix4x4 & orientation() const
Source space point tree item.
const QVector< QVector3D > & positions() const
Tree item representing a FreeSurfer cortical surface in the 3-D scene hierarchy.
FSLIB::FsSurface surfaceData() const
FSLIB::FsAnnotation annotationData() const
Renderable cortical surface mesh with per-vertex color, curvature data, and GPU buffer management.
static constexpr VisualizationMode ModeSurface
DISP3DLIB::VisualizationMode VisualizationMode
void boundingBox(QVector3D &min, QVector3D &max) const
static constexpr VisualizationMode ModeScientific
Data model for a single 2-D MRI volume slice.
Definition sliceobject.h:82
void colorsAvailable(const QString &surfaceKey, const QVector< uint32_t > &colors)
void loadingProgress(int percent, const QString &message)
void loaded(int numTimePoints)
void realtimeColorsAvailable(const QVector< uint32_t > &colorsLh, const QVector< uint32_t > &colorsRh)
void timePointChanged(int index, float time)
void thresholdsUpdated(float min, float mid, float max)
static constexpr ShaderMode Holographic
DISP3DLIB::ShaderMode ShaderMode
Aggregated GPU resources and render state for the 3-D brain visualization scene.
void setMriSlicesVisible(bool visible)
void initialize(QRhiCommandBuffer *cb) override
void setTimePoint(int index)
void setRealtimeSensorLooping(bool enabled)
bool loadNetwork(const CONNECTIVITYLIB::Network &network, const QString &name="Network")
void resetViewportCameraState(int index)
bool isVideoOverlayEnabled() const
void setProbeVisualization(const QVector3D &tip, const QVector3D &direction, float length, const QColor &color, const QColor &glowColor=QColor(0, 0, 0, 0), const QQuaternion &orientation=QQuaternion(0, 0, 0, 0))
void setSourceSpaceVisible(bool visible)
void setMegHelmetOverride(const QString &path)
void surfacePointClicked(const QVector3D &worldPos)
void keyPressEvent(QKeyEvent *event) override
bool isRealtimeSensorStreaming() const
int viewportCameraPreset(int index) const
void setRealtimeSensorAverages(int numAvr)
void setVideoOverlaySize(float meters)
void visualizationEditTargetChanged(int target)
QString bemShaderModeForTarget(int target) const
void setVisualizationEditTarget(int target)
void wheelEvent(QWheelEvent *event) override
bool loadTransformation(const QString &transPath)
void setSlice(int slotIndex, DISP3DLIB::SliceObject *slice)
void setSensorFieldContourVisible(const QString &type, bool visible)
void setBemHighContrast(bool enabled)
void setVisualizationMode(const QString &mode)
void setVideoOverlayUpHint(const QVector3D &dir)
void pushVideoDepthFrame(const QImage &depthFrame)
bool loadDipoles(const QString &dipPath)
bool isViewportEnabled(int index) const
void setSourceThresholds(float min, float mid, float max)
void setBemShaderMode(const QString &mode)
void setSensorTransEnabled(bool enabled)
bool loadSensors(const QString &fifPath)
void timePointChanged(int index, float time)
void startRealtimeSensorStreaming(const QString &modality=QStringLiteral("MEG"))
void setNetworkVisible(bool visible)
void mouseDoubleClickEvent(QMouseEvent *event) override
void surfacePointDoubleClicked(const QVector3D &worldPos)
void setVideoDepthEnabled(bool enabled)
void setRealtimeLooping(bool enabled)
void setModel(BrainTreeModel *model)
void sourceEstimateLoaded(int numTimePoints)
void setRealtimeSensorColormap(const QString &name)
void setHeadMovementPath(const QVector< Eigen::Vector3f > &vecPositions)
void resizeEvent(QResizeEvent *event) override
void setSourceColormap(const QString &name)
void pushVideoOverlayFrame(const QImage &frame)
int closestStcIndex(float timeSec) const
bool megFieldMapOnHeadForTarget(int target) const
void setViewportEnabled(int index, bool enabled)
void setSensorFieldColormap(const QString &name)
void setSensorFieldTimePoint(int index)
bool objectVisibleForTarget(const QString &object, int target) const
void setVideoOverlayEnabled(bool enabled)
void setDipoleVisible(bool visible)
void setVideoDepthSteps(int steps)
QString activeSurfaceForTarget(int target) const
void resetSingleViewCameraState()
void pushRealtimeSourceData(const Eigen::VectorXd &matData)
void viewCountChanged(int count)
void setInitialCameraRotation(const QQuaternion &rotation)
QString overlayModeForTarget(int target) const
void setSensorFieldVisible(const QString &type, bool visible)
bool takeScreenshot(const QString &fileName)
void shaderModeChanged(const QString &modeName)
void render(QRhiCommandBuffer *cb) override
float stcTmin() const
void setHemiVisible(int hemiIdx, bool visible)
void sensorFieldLoaded(int numTimePoints, int initialTimePoint=0)
BrainView(QWidget *parent=nullptr)
Definition brainview.cpp:97
void castRay(const QPoint &pos)
bool intersectWorldRay(const QVector3D &origin, const QVector3D &direction, QVector3D &hitPoint) const
Intersect a world-space ray with loaded scene geometry.
void hoveredRegionChanged(const QString &regionName)
void mousePressEvent(QMouseEvent *event) override
QString shaderModeForTarget(int target) const
void setShaderMode(const QString &mode)
void setViewCount(int count)
void setInfoPanelVisible(bool visible)
static QStringList probeEvokedSets(const QString &evokedPath)
void setRealtimeSensorInterval(int msec)
bool sensorFieldTimeRange(float &tmin, float &tmax) const
void setLightingEnabled(bool enabled)
void mouseMoveEvent(QMouseEvent *event) override
QMap< int, QVector3D > cardinalFiducialsInMri() const
Return cardinal fiducials (NAS/LPA/RPA) transformed to MRI coordinates.
void setMegFieldMapOnHead(bool useHead)
void setVideoOverlayOpacity(float opacity)
void setLiveRay(const QVector3D &from, const QVector3D &to, const QColor &color, float radius=0.001f)
bool savePng(const QString &path, int width=1200, int height=800, const QString &surfaceType=QStringLiteral("pial"))
void stopRealtimeSensorStreaming()
int visualizationEditTarget() const
void setViewportCameraPreset(int index, int preset)
void onRowsInserted(const QModelIndex &parent, int first, int last)
bool loadSourceEstimate(const QString &lhPath, const QString &rhPath)
void syncBemShadersToBrainShaders()
bool bemTopVertexInMri(QVector3D &pos) const
Return the highest-Z vertex of the BEM head surface in MRI coords.
void setAutomatedRotation(bool enabled)
void setActiveSurface(const QString &type)
bool loadSensorField(const QString &evokedPath, int aveIndex=0)
void setRealtimeInterval(int msec)
void setVideoOverlayFocusPosition(const QVector3D &position)
void setLiveMarkers(const QVector< LiveMarker > &markers)
void setBemVisible(const QString &name, bool visible)
bool isRealtimeStreaming() const
void stcLoadingProgress(int percent, const QString &message)
bool loadMegHelmetSurface(const QString &helmetFilePath)
float stcStep() const
int closestSensorFieldIndex(float timeSec) const
void mouseReleaseEvent(QMouseEvent *event) override
void setStaticMarkers(const QVector< LiveMarker > &markers)
void setSensorVisible(const QString &type, bool visible)
void setVideoDepthScale(float scale)
void setCameraFocusOverride(const QVector3D &center, float size)
bool automatedRotation() const
int stcNumTimePoints() const
void setNetworkColormap(const QString &name)
void sensorFieldTimePointChanged(int index, float time)
void pushRealtimeSensorData(const Eigen::VectorXf &vecData)
bool loadSourceSpace(const QString &fwdPath)
void setNetworkThreshold(double threshold)
void onDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector< int > &roles)
void sourceThresholdsUpdated(float min, float mid, float max)
void setSliceVisible(int slotIndex, bool visible)
static Qt::CursorShape cursorForHit(SplitterHit hit)
Labelled 4x4 FIFF affine: source frame, destination frame, rotation, translation and cached inverse.
Eigen::Matrix< float, 4, 4, Eigen::DontAlign > trans
Single averaged evoked response: time axis, data, baseline, channel info and averaging metadata.
Definition fiff_evoked.h:77
BEM surface provides geometry information.