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