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