#include "nmWxPebiResultViewWidget.h" #include "nmVolumeImportTask.h" #include "nmVolumeSceneController.h" #include "nmVolumeSliceInteraction.h" #include "nmWxVolumeSliceSettingsDlg.h" #include "nmVTKViewState.h" #include #include #include #include #include #include #include "iUnitGroup.h" #include "iUnitHelper.h" #include "nmDataAnalyzeManager.h" #include "nmFFmpegVideoWriter.h" #include "nmPebiRegionPressureCalculator.h" #include "nmPebiResultAnimationExporter.h" #include "nmPebiResultDisplaySettings.h" #include "nmPebiResultPlaybackController.h" #include "nmPebiResultPropertyCatalog.h" #include "nmPebiResultPropertyDisplayText.h" #include "nmPebiResultSceneController.h" #include "nmPebiResultSnapshot.h" #include "nmVTKMeshLayer.h" #include "nmVTKPlanarPolygonInteractorStyle.h" #include "nmVTKPlanarPolygonLayer.h" #include "nmVTKScene.h" #include "nmVTKViewCommandController.h" #include "nmWxPebiResultControlBar.h" #include "nmWxPebiResultAnimationExportDlg.h" #include "nmWxVTKRenderContainerWidget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if !defined(_MSC_VER) #include #endif namespace { QAtomicInt g_nPebiAnimationExportActive(0); const char* const PEBI_REGION_PRESSURE_LAYER_ID = "PebiResult.RegionPressurePolygon"; QString resultViewIconPath(const QString& sFileName) { const QString sApplicationDir = QCoreApplication::applicationDirPath(); return sApplicationDir.section('/', 0, -2) + "/Res/Icon/" + sFileName; } bool isResultViewGuiThread() { QCoreApplication* pApplication = QCoreApplication::instance(); return pApplication == nullptr || pApplication->thread() == QThread::currentThread(); } void setActionCheckedWithoutSignal(QAction* pAction, bool bChecked) { if(pAction == nullptr) { return; } const bool bBlocked = pAction->blockSignals(true); pAction->setChecked(bChecked); pAction->blockSignals(bBlocked); } bool isFiniteFilterEditorValue(double dValue) { #if defined(_MSC_VER) return _finite(dValue) != 0; #else return std::isfinite(dValue); #endif } QString formatRegionValue(double dValue) { if(!isFiniteFilterEditorValue(dValue)) { return QString(); } return QLocale().toString(dValue, 'g', 10); } QLineEdit* addRegionStatisticsRow(QGridLayout* pLayout, int nRow, const QString& sName, QWidget* pParent, QWidget* pUnitWidget = nullptr) { QLabel* pNameLabel = new QLabel(sName, pParent); QLineEdit* pValueEdit = new QLineEdit(pParent); pNameLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); pValueEdit->setReadOnly(true); pValueEdit->setAlignment(Qt::AlignRight | Qt::AlignVCenter); pValueEdit->setMinimumWidth(150); pLayout->addWidget(pNameLabel, nRow, 0); pLayout->addWidget(pValueEdit, nRow, 1); if(pUnitWidget != nullptr) { pLayout->addWidget(pUnitWidget, nRow, 2); } return pValueEdit; } QComboBox* createRegionUnitCombo(QWidget* pParent, const QString& sUnit) { QComboBox* pCombo = new QComboBox(pParent); pCombo->addItem(sUnit); pCombo->setEnabled(false); pCombo->setMinimumWidth(80); pCombo->setMaximumWidth(140); pCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); pCombo->setSizeAdjustPolicy(QComboBox::AdjustToContentsOnFirstShow); return pCombo; } bool canProjectRegionOntoXyPlane(vtkRenderer* pRenderer) { vtkCamera* pCamera = pRenderer == nullptr ? nullptr : pRenderer->GetActiveCamera(); if(pCamera == nullptr) { return false; } double dDirection[3]; pCamera->GetDirectionOfProjection(dDirection); const double dLength = vtkMath::Norm(dDirection); return vtkMath::IsFinite(dLength) && dLength > 0.0 && vtkMath::IsFinite(dDirection[2]) && qAbs(dDirection[2]) > 1.0e-10 * dLength; } class AnimationExportGuard { public: explicit AnimationExportGuard(nmWxPebiResultControlBar* pControlBar) : m_pControlBar(pControlBar), m_bAcquired(g_nPebiAnimationExportActive.testAndSetOrdered(0, 1)) { if(m_bAcquired && m_pControlBar != nullptr) { m_pControlBar->setExportInProgress(true); } } ~AnimationExportGuard() { release(); } void release() { if(!m_bAcquired) { return; } if(m_pControlBar != nullptr) { m_pControlBar->setExportInProgress(false); } g_nPebiAnimationExportActive.fetchAndStoreOrdered(0); m_bAcquired = false; } bool isAcquired() const { return m_bAcquired; } private: QPointer m_pControlBar; bool m_bAcquired; }; } class nmWxPebiResultRegionState { public: enum State { State_Idle = 0, State_Drawing, State_Completed }; enum PendingEvent { PendingEvent_None = 0, PendingEvent_Completed, PendingEvent_Cancelled, PendingEvent_Invalid }; nmWxPebiResultRegionState() : m_eState(State_Idle), m_ePendingEvent(PendingEvent_None), m_bPendingEventQueued(false), m_pLayer(nullptr), m_nChangedObserverTag(0), m_nCompletedObserverTag(0), m_nCancelledObserverTag(0), m_nInvalidObserverTag(0), m_nFrozenBindingRevision(0), m_nFrozenFrameIndex(-1), m_nRegionBindingRevision(0), m_nLastAttemptedFrameIndex(-1), m_bStatisticsCalculated(false), m_pDialog(nullptr), m_pAveragePressureUnitCombo(nullptr), m_pMinimumPressureUnitCombo(nullptr), m_pMaximumPressureUnitCombo(nullptr), m_pPressureTimeUnitCombo(nullptr), m_pAveragePressureValue(nullptr), m_pMinimumPressureValue(nullptr), m_pMaximumPressureValue(nullptr), m_pTimeValue(nullptr), m_pSelectedCellValue(nullptr), m_pWarningLabel(nullptr) { } State m_eState; ///< 当前窗口区域工具状态。 PendingEvent m_ePendingEvent; ///< 等待 Qt 事件循环处理的 VTK 终止事件。 bool m_bPendingEventQueued; ///< 是否已有一次安全收尾被排队。 QVector m_vecPendingPoints; ///< 完成回调复制出的候选世界坐标。 nmPebiRegionPressureCalculator m_oCalculator; ///< 独占相交缓存和压力计算器。 nmVTKPlanarPolygonLayer* m_pLayer; ///< 业务 Scene 所有的区域轮廓观察指针。 vtkSmartPointer m_pStyle; ///< 临时绘制样式。 vtkSmartPointer m_pCallback; ///< 不拥有窗口的 VTK 回调命令。 unsigned long m_nChangedObserverTag; ///< 点集变化观察者标识。 unsigned long m_nCompletedObserverTag; ///< 绘制完成观察者标识。 unsigned long m_nCancelledObserverTag; ///< 绘制取消观察者标识。 unsigned long m_nInvalidObserverTag; ///< 无效完成观察者标识。 QSharedPointer m_pFrozenSnapshot; ///< 绘制开始时的快照强引用。 quint64 m_nFrozenBindingRevision; ///< 绘制开始时的成功绑定序号。 QString m_sFrozenSnapshotId; ///< 绘制开始时的快照稳定 ID。 QString m_sFrozenPropertyId; ///< 绘制开始时的显示属性 ID。 int m_nFrozenFrameIndex; ///< 绘制开始时的 pressure 帧。 quint64 m_nRegionBindingRevision; ///< 已完成区域所属成功绑定序号。 QString m_sRegionSnapshotId; ///< 已完成区域所属快照 ID。 QString m_sLastObservedPropertyId; ///< 最近一次状态信号观察到的显示属性。 int m_nLastAttemptedFrameIndex; ///< 最近一次尝试统计的 pressure 帧。 bool m_bStatisticsCalculated; ///< 最近一次统计调用是否成功。 QString m_sStatisticsError; ///< 最近一次统计调用失败说明。 nmPebiRegionPressureStatistics m_oStatistics; ///< 最近一次成功统计结果。 QDialog* m_pDialog; ///< ViewWidget 子对象,关闭时同步结束本次统计。 QComboBox* m_pAveragePressureUnitCombo; ///< 固定显示 MPa 的禁用单位框。 QComboBox* m_pMinimumPressureUnitCombo; ///< 压力下限固定单位框。 QComboBox* m_pMaximumPressureUnitCombo; ///< 压力上限固定单位框。 QComboBox* m_pPressureTimeUnitCombo; ///< 固定显示 h 的禁用单位框。 QLineEdit* m_pAveragePressureValue; ///< 只读区域平均压力。 QLineEdit* m_pMinimumPressureValue; ///< 只读区域压力下限。 QLineEdit* m_pMaximumPressureValue; ///< 只读区域压力上限。 QLineEdit* m_pTimeValue; ///< 只读求解时间显示值。 QLineEdit* m_pSelectedCellValue; ///< 只读相交单元数。 QLabel* m_pWarningLabel; ///< 仅在无结果或有忽略单元时显示的说明。 }; nmWxPebiResultFilterSettingsDlg::nmWxPebiResultFilterSettingsDlg( nmPebiResultSceneController* pSceneController, nmWxPebiResultControlBar* pControlBar, QWidget* pParent) : iDlgBase(pParent), m_pSceneController(pSceneController), m_pControlBar(pControlBar), m_nExpectedBindingRevision(0), m_nExpectedDisplayFrameIndex(-1), m_pUnitGroup(nullptr), m_bInitialized(false), m_pEnableCheck(nullptr), m_pPropertyCombo(nullptr), m_pMinimumCheck(nullptr), m_pMinimumEdit(nullptr), m_pMaximumCheck(nullptr), m_pMaximumEdit(nullptr), m_pUnitCombo(nullptr), m_pApplyButton(nullptr), m_pOkButton(nullptr) { createUi(); m_bInitialized = initializeFromScene(); if(!m_bInitialized) { m_pApplyButton->setEnabled(false); m_pOkButton->setEnabled(false); QMessageBox::warning( this, tr("Filtering settings"), tr("Unable to read the current result filter settings.")); } } nmWxPebiResultFilterSettingsDlg::~nmWxPebiResultFilterSettingsDlg() { } void nmWxPebiResultFilterSettingsDlg::createUi() { setWindowTitle(tr("Filtering settings")); setModal(true); setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); m_pEnableCheck = new QCheckBox(tr("Enable filtering"), this); m_pPropertyCombo = new QComboBox(this); m_pPropertyCombo->setSizeAdjustPolicy( QComboBox::AdjustToContentsOnFirstShow); m_pPropertyCombo->setMinimumWidth(220); m_pMinimumCheck = new QCheckBox(tr("Above min (>=)"), this); m_pMinimumEdit = new QLineEdit(this); m_pMinimumEdit->setAlignment(Qt::AlignRight); m_pMaximumCheck = new QCheckBox(tr("Below max (<=)"), this); m_pMaximumEdit = new QLineEdit(this); m_pMaximumEdit->setAlignment(Qt::AlignRight); m_pUnitCombo = new QComboBox(this); m_pUnitCombo->setMinimumWidth(80); m_pUnitCombo->setMaximumWidth(140); m_pUnitCombo->hide(); m_pMinimumEdit->setMinimumWidth(150); m_pMaximumEdit->setMinimumWidth(150); QGridLayout* pFieldsLayout = new QGridLayout(); pFieldsLayout->setContentsMargins(0, 0, 0, 0); pFieldsLayout->setHorizontalSpacing(8); pFieldsLayout->setVerticalSpacing(6); pFieldsLayout->addWidget(m_pEnableCheck, 0, 0, 1, 3); QLabel* pPropertyLabel = new QLabel(tr("Filter on"), this); pPropertyLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); pFieldsLayout->addWidget(pPropertyLabel, 1, 0); pFieldsLayout->addWidget(m_pPropertyCombo, 1, 1, 1, 2); pFieldsLayout->addWidget(m_pMinimumCheck, 2, 0); pFieldsLayout->addWidget(m_pMinimumEdit, 2, 1); pFieldsLayout->addWidget(m_pUnitCombo, 2, 2, 2, 1, Qt::AlignVCenter); pFieldsLayout->addWidget(m_pMaximumCheck, 3, 0); pFieldsLayout->addWidget(m_pMaximumEdit, 3, 1); pFieldsLayout->setColumnStretch(1, 1); // 普通布局不受平台按钮顺序策略影响,始终保持应用、确定、取消。 QHBoxLayout* pButtonLayout = new QHBoxLayout(); pButtonLayout->addStretch(); m_pApplyButton = new QPushButton(tr("Apply"), this); m_pOkButton = new QPushButton(tr("OK"), this); QPushButton* pCancelButton = new QPushButton(tr("Cancel"), this); const QSize oFooterIconSize(16, 16); m_pOkButton->setIcon(QIcon(resultViewIconPath("OK.png"))); pCancelButton->setIcon(QIcon(resultViewIconPath("Cancel.png"))); m_pOkButton->setIconSize(oFooterIconSize); pCancelButton->setIconSize(oFooterIconSize); m_pApplyButton->setMinimumWidth(72); m_pOkButton->setMinimumWidth(72); pCancelButton->setMinimumWidth(72); m_pApplyButton->setAutoDefault(false); m_pOkButton->setDefault(true); pCancelButton->setAutoDefault(false); pButtonLayout->setContentsMargins(4, 2, 4, 5); pButtonLayout->setSpacing(6); pButtonLayout->addWidget(m_pApplyButton); pButtonLayout->addWidget(m_pOkButton); pButtonLayout->addWidget(pCancelButton); QVBoxLayout* pMainLayout = new QVBoxLayout(this); pMainLayout->setContentsMargins(12, 10, 12, 0); pMainLayout->setSpacing(8); pMainLayout->addLayout(pFieldsLayout); pMainLayout->addLayout(pButtonLayout); setMinimumWidth(430); connect(m_pApplyButton, SIGNAL(clicked()), this, SLOT(onApplyClicked())); connect(m_pOkButton, SIGNAL(clicked()), this, SLOT(onOkClicked())); connect(pCancelButton, SIGNAL(clicked()), this, SLOT(reject())); connect(m_pPropertyCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onPropertyChanged(int))); connect(m_pUnitCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onDisplayUnitChanged(int))); connect(m_pEnableCheck, SIGNAL(toggled(bool)), this, SLOT(refreshEditorEnabledState())); connect(m_pMinimumCheck, SIGNAL(toggled(bool)), this, SLOT(refreshEditorEnabledState())); connect(m_pMaximumCheck, SIGNAL(toggled(bool)), this, SLOT(refreshEditorEnabledState())); } bool nmWxPebiResultFilterSettingsDlg::initializeFromScene() { if(m_pSceneController == nullptr || m_pControlBar == nullptr || !m_pSceneController->hasSnapshot()) { return false; } m_nExpectedBindingRevision = m_pSceneController->getSnapshotBindingRevision(); m_sExpectedSnapshotId = m_pSceneController->getCurrentSnapshotId(); m_sExpectedDisplayPropertyId = m_pSceneController->getCurrentPropertyId(); m_nExpectedDisplayFrameIndex = m_pSceneController->getCurrentFrameIndex(); if(m_nExpectedBindingRevision == 0 || m_sExpectedSnapshotId.isEmpty() || m_sExpectedDisplayPropertyId.isEmpty() || m_nExpectedDisplayFrameIndex < 0 || !m_pSceneController->getFilterSettings(m_oBaselineSettings)) { return false; } const bool bSignalsBlocked = m_pPropertyCombo->blockSignals(true); const QStringList listPropertyIds = m_pSceneController->getAvailablePropertyIds(); for(int nIndex = 0; nIndex < listPropertyIds.size(); ++nIndex) { const QString& sPropertyId = listPropertyIds[nIndex]; double dRange[2] = { 0.0, 0.0 }; if(!m_pSceneController->getPropertyRange(sPropertyId, dRange)) { m_pPropertyCombo->blockSignals(bSignalsBlocked); return false; } m_mapPropertyRanges.insert( sPropertyId, qMakePair(dRange[0], dRange[1])); QString sDisplayName = nmPebiResultPropertyDisplayText::getDisplayName(sPropertyId); if(sDisplayName.isEmpty()) { sDisplayName = sPropertyId; } m_pPropertyCombo->addItem(sDisplayName, sPropertyId); } m_pPropertyCombo->blockSignals(bSignalsBlocked); if(m_pPropertyCombo->count() <= 0) { return false; } if(m_oBaselineSettings.m_sPropertyId.isEmpty() || !m_mapPropertyRanges.contains( m_oBaselineSettings.m_sPropertyId)) { m_oBaselineSettings.m_sPropertyId = m_sExpectedDisplayPropertyId; const QPair oRange = m_mapPropertyRanges.value( m_oBaselineSettings.m_sPropertyId); m_oBaselineSettings.m_bMinimumEnabled = true; m_oBaselineSettings.m_dMinimum = oRange.first; m_oBaselineSettings.m_bMaximumEnabled = true; m_oBaselineSettings.m_dMaximum = oRange.second; } return populateSettings(m_oBaselineSettings); } bool nmWxPebiResultFilterSettingsDlg::initializeUnitEditor( const QString& sPropertyId) { nmPebiResultPropertyDescriptor oDescriptor; if(!nmPebiResultPropertyCatalog::findDescriptor( sPropertyId, oDescriptor)) { return false; } const bool bSignalsBlocked = m_pUnitCombo->blockSignals(true); m_pUnitCombo->clear(); m_pUnitGroup = nullptr; m_sBaseUnit = oDescriptor.m_sBaseUnit; m_sDisplayUnit = oDescriptor.m_sDefaultDisplayUnit.isEmpty() ? m_sBaseUnit : oDescriptor.m_sDefaultDisplayUnit; if(m_sBaseUnit.isEmpty()) { m_sDisplayUnit.clear(); m_pUnitCombo->setEnabled(false); m_pUnitCombo->hide(); m_pUnitCombo->blockSignals(bSignalsBlocked); return true; } m_pUnitGroup = iUnitHelper::getUnitGroupByUnit(m_sBaseUnit); QStringList listUnits; if(m_pUnitGroup != nullptr && m_pUnitGroup->indexOf(m_sBaseUnit) >= 0) { listUnits = m_pUnitGroup->getAllUnitNames(); } const bool bHasUnitGroup = !listUnits.isEmpty() && listUnits.contains(m_sBaseUnit); if(!bHasUnitGroup) { m_pUnitGroup = nullptr; listUnits.clear(); listUnits.append(m_sBaseUnit); m_sDisplayUnit = m_sBaseUnit; } else if(!listUnits.contains(m_sDisplayUnit)) { m_sDisplayUnit = m_sBaseUnit; } m_pUnitCombo->addItems(listUnits); m_pUnitCombo->setCurrentIndex( m_pUnitCombo->findText(m_sDisplayUnit)); m_pUnitCombo->setEnabled(bHasUnitGroup); m_pUnitCombo->show(); m_pUnitCombo->blockSignals(bSignalsBlocked); return true; } bool nmWxPebiResultFilterSettingsDlg::populateSettings( const nmPebiResultFilterSettings& oSettings) { const bool bPropertySignalsBlocked = m_pPropertyCombo->blockSignals(true); const int nPropertyIndex = m_pPropertyCombo->findData( oSettings.m_sPropertyId); if(nPropertyIndex < 0 || !initializeUnitEditor(oSettings.m_sPropertyId)) { m_pPropertyCombo->blockSignals(bPropertySignalsBlocked); return false; } m_pPropertyCombo->setCurrentIndex(nPropertyIndex); m_pPropertyCombo->blockSignals(bPropertySignalsBlocked); double dDisplayMinimum = 0.0; double dDisplayMaximum = 0.0; if(!convertUnitValue( oSettings.m_dMinimum, m_sBaseUnit, m_sDisplayUnit, dDisplayMinimum) || !convertUnitValue( oSettings.m_dMaximum, m_sBaseUnit, m_sDisplayUnit, dDisplayMaximum)) { return false; } m_pEnableCheck->setChecked(oSettings.m_bEnabled); m_pMinimumCheck->setChecked(oSettings.m_bMinimumEnabled); m_pMaximumCheck->setChecked(oSettings.m_bMaximumEnabled); m_pMinimumEdit->setText(formatValue(dDisplayMinimum)); m_pMaximumEdit->setText(formatValue(dDisplayMaximum)); refreshEditorEnabledState(); return true; } void nmWxPebiResultFilterSettingsDlg::onApplyClicked() { applyEditor(); } void nmWxPebiResultFilterSettingsDlg::onOkClicked() { if(applyEditor()) { accept(); } } void nmWxPebiResultFilterSettingsDlg::onPropertyChanged(int nIndex) { if(nIndex >= 0 && !setPropertyRange( m_pPropertyCombo->itemData(nIndex).toString())) { QMessageBox::warning( this, tr("Filtering settings"), tr("Unable to convert the filter limits " "to the selected unit.")); } } void nmWxPebiResultFilterSettingsDlg::onDisplayUnitChanged(int nIndex) { if(nIndex < 0) { return; } const QString sNewUnit = m_pUnitCombo->itemText(nIndex); if(sNewUnit == m_sDisplayUnit) { return; } double dOldDisplayMinimum = 0.0; double dOldDisplayMaximum = 0.0; double dBaseMinimum = 0.0; double dBaseMaximum = 0.0; double dNewDisplayMinimum = 0.0; double dNewDisplayMaximum = 0.0; const bool bConverted = parseFiniteValue( m_pMinimumEdit->text(), dOldDisplayMinimum) && parseFiniteValue( m_pMaximumEdit->text(), dOldDisplayMaximum) && convertUnitValue( dOldDisplayMinimum, m_sDisplayUnit, m_sBaseUnit, dBaseMinimum) && convertUnitValue( dOldDisplayMaximum, m_sDisplayUnit, m_sBaseUnit, dBaseMaximum) && convertUnitValue( dBaseMinimum, m_sBaseUnit, sNewUnit, dNewDisplayMinimum) && convertUnitValue( dBaseMaximum, m_sBaseUnit, sNewUnit, dNewDisplayMaximum); if(!bConverted) { const bool bSignalsBlocked = m_pUnitCombo->blockSignals(true); m_pUnitCombo->setCurrentIndex( m_pUnitCombo->findText(m_sDisplayUnit)); m_pUnitCombo->blockSignals(bSignalsBlocked); QMessageBox::warning( this, tr("Filtering settings"), tr("Unable to convert the filter limits " "to the selected unit.")); return; } m_sDisplayUnit = sNewUnit; m_pMinimumEdit->setText(formatValue(dNewDisplayMinimum)); m_pMaximumEdit->setText(formatValue(dNewDisplayMaximum)); } void nmWxPebiResultFilterSettingsDlg::refreshEditorEnabledState() { const bool bEnabled = m_pEnableCheck->isChecked(); m_pPropertyCombo->setEnabled(bEnabled); m_pMinimumCheck->setEnabled(bEnabled); m_pMaximumCheck->setEnabled(bEnabled); m_pMinimumEdit->setEnabled( bEnabled && m_pMinimumCheck->isChecked()); m_pMaximumEdit->setEnabled( bEnabled && m_pMaximumCheck->isChecked()); m_pUnitCombo->setEnabled( bEnabled && m_pUnitGroup != nullptr); } bool nmWxPebiResultFilterSettingsDlg::collectSettings( nmPebiResultFilterSettings& oSettings, QString& sError) const { sError.clear(); const int nPropertyIndex = m_pPropertyCombo->currentIndex(); if(nPropertyIndex < 0) { sError = tr("A filter property must be selected."); return false; } oSettings.m_bEnabled = m_pEnableCheck->isChecked(); oSettings.m_sPropertyId = m_pPropertyCombo->itemData(nPropertyIndex).toString(); oSettings.m_bMinimumEnabled = m_pMinimumCheck->isChecked(); oSettings.m_bMaximumEnabled = m_pMaximumCheck->isChecked(); // 复选框只控制边界是否参与筛选;未启用边界的编辑值仍需保留, // 避免重新打开对话框时退回设置对象的默认 0。 double dDisplayMinimum = 0.0; double dDisplayMaximum = 0.0; if(!parseFiniteValue( m_pMinimumEdit->text(), dDisplayMinimum) || !parseFiniteValue( m_pMaximumEdit->text(), dDisplayMaximum)) { sError = tr("Filter limits must be finite numbers."); return false; } if(!convertUnitValue( dDisplayMinimum, m_sDisplayUnit, m_sBaseUnit, oSettings.m_dMinimum) || !convertUnitValue( dDisplayMaximum, m_sDisplayUnit, m_sBaseUnit, oSettings.m_dMaximum)) { sError = tr( "Unable to convert the filter limits " "to the selected unit."); return false; } if(oSettings.m_bMinimumEnabled && oSettings.m_bMaximumEnabled && oSettings.m_dMinimum > oSettings.m_dMaximum) { sError = tr("The minimum filter limit cannot exceed the maximum."); return false; } return true; } bool nmWxPebiResultFilterSettingsDlg::applyEditor() { nmPebiResultPlaybackController* pPlaybackController = m_pControlBar == nullptr ? nullptr : m_pControlBar->getPlaybackController(); if(!m_bInitialized || m_pSceneController == nullptr || m_pControlBar == nullptr || pPlaybackController == nullptr || pPlaybackController->isPlaying() || m_pControlBar->isExportInProgress()) { const QString sError = tr( "Filtering is unavailable during playback or export."); QMessageBox::warning(this, tr("Filtering settings"), sError); return false; } nmPebiResultFilterSettings oSettings; QString sError; if(!collectSettings(oSettings, sError) || !m_pSceneController->applyFilterSettings( m_nExpectedBindingRevision, m_sExpectedSnapshotId, m_sExpectedDisplayPropertyId, m_nExpectedDisplayFrameIndex, oSettings, &sError)) { if(sError.isEmpty()) { sError = tr("Unable to apply the filter settings."); } QMessageBox::warning(this, tr("Filtering settings"), sError); return false; } m_oBaselineSettings = oSettings; emit filterSettingsApplied(); return true; } bool nmWxPebiResultFilterSettingsDlg::setPropertyRange( const QString& sPropertyId) { if(!m_mapPropertyRanges.contains(sPropertyId) || !initializeUnitEditor(sPropertyId)) { return false; } const QPair oRange = m_mapPropertyRanges.value(sPropertyId); double dDisplayMinimum = 0.0; double dDisplayMaximum = 0.0; if(!convertUnitValue( oRange.first, m_sBaseUnit, m_sDisplayUnit, dDisplayMinimum) || !convertUnitValue( oRange.second, m_sBaseUnit, m_sDisplayUnit, dDisplayMaximum)) { return false; } m_pMinimumCheck->setChecked(true); m_pMaximumCheck->setChecked(true); m_pMinimumEdit->setText(formatValue(dDisplayMinimum)); m_pMaximumEdit->setText(formatValue(dDisplayMaximum)); refreshEditorEnabledState(); return true; } QString nmWxPebiResultFilterSettingsDlg::formatValue(double dValue) const { return QLocale::c().toString(dValue, 'g', 12); } bool nmWxPebiResultFilterSettingsDlg::parseFiniteValue( const QString& sText, double& dValue) const { bool bOk = false; const double dCandidate = QLocale::c().toDouble( sText.trimmed(), &bOk); if(!bOk || !isFiniteFilterEditorValue(dCandidate)) { return false; } dValue = dCandidate; return true; } bool nmWxPebiResultFilterSettingsDlg::convertUnitValue( double dSourceValue, const QString& sSourceUnit, const QString& sDestinationUnit, double& dDestinationValue) const { if(!isFiniteFilterEditorValue(dSourceValue)) { return false; } if(sSourceUnit == sDestinationUnit) { dDestinationValue = dSourceValue; return true; } if(sSourceUnit.isEmpty() || sDestinationUnit.isEmpty() || m_pUnitGroup == nullptr || m_pUnitGroup->indexOf(sSourceUnit) < 0 || m_pUnitGroup->indexOf(sDestinationUnit) < 0) { return false; } int nDigit = 12; double dCandidate = 0.0; if(!m_pUnitGroup->convert( sSourceUnit, dSourceValue, sDestinationUnit, dCandidate, nDigit) || !isFiniteFilterEditorValue(dCandidate)) { return false; } dDestinationValue = dCandidate; return true; } /** @brief 外部三维模式的窗口私有状态,所有成员仅由 GUI 线程管理。 */ class nmWxPebiVolumeState { public: /** @brief 初始化为空模式;控制器在通用渲染容器创建后绑定。 */ nmWxPebiVolumeState() : m_pController(nullptr), m_pTask(nullptr), m_pProgress(nullptr), m_pImportAction(nullptr), m_pReturnAction(nullptr), m_pButton(nullptr), m_pSliceAction(nullptr), m_pSliceSettingsAction(nullptr), m_pSliceDialog(nullptr), m_pSliceInteraction(nullptr), m_bImported(false), m_bPendingSync(false), m_bRegionVisible(false), m_bRegionDialogVisible(false) {} nmVolumeSceneController* m_pController; ///< 窗口独占,必须在渲染容器之前析构。 nmVolumeImportTask* m_pTask; ///< 当前唯一导入线程,结束后才允许读取结果。 QProgressDialog* m_pProgress; ///< 临时进度框,窗口对象树拥有。 QAction* m_pImportAction; ///< 按钮菜单和右键菜单共享的导入动作。 QAction* m_pReturnAction; ///< 仅外部模型模式启用的返回动作。 QToolButton* m_pButton; ///< 唯一新增的常驻切片工具按钮。 QAction* m_pSliceAction; ///< 按钮菜单和右键菜单共享的切片显隐动作。 QAction* m_pSliceSettingsAction; ///< 打开浮动切片参数窗口的共享动作。 nmWxVolumeSliceSettingsDlg* m_pSliceDialog; ///< 按需创建并复用的非模态参数窗口。 nmVolumeSliceInteraction* m_pSliceInteraction; ///< 窗口独占,负责辅助平面及安全事件转发。 nmVTKViewState m_oSavedView; ///< 第一次进入外部模式前捕获的计算结果视图。 nmVTKViewCommandController::ViewCommands m_eSavedCommands; ///< 原结果提供的视图命令能力。 bool m_bImported; ///< 当前画面是否为外部三维模型。 bool m_bPendingSync; ///< 外部模式期间发生 Manager 变化,返回时重新读取。 bool m_bRegionVisible; ///< 原区域轮廓的实际可见状态。 bool m_bRegionDialogVisible; ///< 原区域统计面板是否可见。 }; nmWxPebiResultViewWidget::nmWxPebiResultViewWidget( nmDataAnalyzeManager* pDataManager, QWidget* pParent) : QWidget(pParent), m_pRenderContainer(nullptr), m_pSceneController(nullptr), m_pControlBar(nullptr), m_pToolBar(nullptr), m_pStatusLabel(nullptr), m_pScalarBarAction(nullptr), m_pScaleGridAction(nullptr), m_pWellNamesAction(nullptr), m_pFilterAction(nullptr), m_pRegionPressureAction(nullptr), m_pRegionState(new nmWxPebiResultRegionState), m_pVolumeState(new nmWxPebiVolumeState), m_bObservedSnapshotCurrent(false), m_bObservedStateValid(false), m_bSynchronizing(false), m_bSyncPending(false) { createUi(); setDataManager(pDataManager); } nmWxPebiResultViewWidget::~nmWxPebiResultViewWidget() { // 先取消并断开队列回调,等待线程退出后才能释放窗口和相关 DLL 对象。 if(m_pVolumeState->m_pTask != nullptr) { disconnect(m_pVolumeState->m_pTask, nullptr, this, nullptr); m_pVolumeState->m_pTask->requestCancel(); m_pVolumeState->m_pTask->wait(); delete m_pVolumeState->m_pTask; } closeVolumeSliceSettings(true); delete m_pVolumeState->m_pSliceDialog; m_pVolumeState->m_pSliceDialog = nullptr; delete m_pVolumeState->m_pSliceInteraction; m_pVolumeState->m_pSliceInteraction = nullptr; if(m_pControlBar != nullptr) m_pControlBar->setVolumeSceneController(nullptr); delete m_pVolumeState->m_pController; delete m_pVolumeState; m_pVolumeState = nullptr; disconnectDataManager(); destroyRegionPressureState(); if(m_pControlBar != nullptr) { m_pControlBar->stopPlayback(); delete m_pControlBar; m_pControlBar = nullptr; } if(m_pSceneController != nullptr) { m_pSceneController->clear(); delete m_pSceneController; m_pSceneController = nullptr; } // 渲染容器、工具栏、状态栏和 QAction 最后由 QWidget 对象树统一释放。 } void nmWxPebiResultViewWidget::setDataManager( nmDataAnalyzeManager* pDataManager) { Q_ASSERT(isResultViewGuiThread()); Q_ASSERT(pDataManager == nullptr || pDataManager->thread() == QThread::currentThread()); if(!isResultViewGuiThread() || (pDataManager != nullptr && pDataManager->thread() != QThread::currentThread())) { // 窗口会同步读取不可变快照引用,不能跨线程直接访问 Manager。 return; } if(m_pDataManager.data() == pDataManager) { // 生产入口可能重复安装同一 Manager;这里只读最新状态,不能清空相机。 synchronizeFromDataManager(); return; } if(m_pVolumeState->m_pTask != nullptr) m_pVolumeState->m_pTask->requestCancel(); disconnectDataManager(); if(isImportedVolumeMode()) { // 分析上下文换绑时丢弃旧计算结果,但保留用户正在检查的外部模型。 clearRegionPressureSelection(false); m_pSceneController->clear(); m_pDisplayedSnapshot.clear(); m_sDisplayedSnapshotId.clear(); m_pVolumeState->m_oSavedView = nmVTKViewState(); m_pVolumeState->m_bRegionVisible = false; m_pVolumeState->m_bRegionDialogVisible = false; m_pVolumeState->m_bPendingSync = true; } else { clearSceneAndControls(tr("No result data.")); } m_pDataManager = pDataManager; if(!m_pDataManager.isNull()) { connect(m_pDataManager.data(), SIGNAL(sigPebiResultStateChanged()), this, SLOT(onPebiResultStateChanged())); connect(m_pDataManager.data(), SIGNAL(destroyed(QObject*)), this, SLOT(onDataManagerDestroyed(QObject*))); } synchronizeFromDataManager(); } nmDataAnalyzeManager* nmWxPebiResultViewWidget::getDataManager() const { return m_pDataManager.data(); } void nmWxPebiResultViewWidget::synchronizeFromDataManager() { Q_ASSERT(isResultViewGuiThread()); Q_ASSERT(m_pDataManager.isNull() || m_pDataManager->thread() == QThread::currentThread()); if(!isResultViewGuiThread() || (!m_pDataManager.isNull() && m_pDataManager->thread() != QThread::currentThread())) { return; } if(m_bSynchronizing) { // 同步期间只记录一次待处理请求,当前事务结束后重新读取 Manager 最新值。 m_bSyncPending = true; return; } m_bSynchronizing = true; do { m_bSyncPending = false; synchronizeOnce(); } while(m_bSyncPending); m_bSynchronizing = false; } void nmWxPebiResultViewWidget::onPebiResultStateChanged() { synchronizeFromDataManager(); } void nmWxPebiResultViewWidget::onDataManagerDestroyed(QObject* pObject) { Q_UNUSED(pObject); // destroyed() 到达时 QPointer 可能已经清空,槽内绝不再读取 Manager。 m_pDataManager = nullptr; if(isImportedVolumeMode()) { m_pVolumeState->m_bPendingSync = true; return; } if(m_bSynchronizing) { // 极端嵌套事件下不得在快照提交栈内递归清场,退出当前事务后再读空状态。 m_bSyncPending = true; return; } clearSceneAndControls(tr("No result data.")); // 记录稳定空状态,后续重复同步空 Manager 时不再清场或切换状态页。 rememberObservedState( QSharedPointer(), QString(), false); } void nmWxPebiResultViewWidget::appendContextMenuActions(QMenu* pMenu) { if(pMenu == nullptr) { return; } pMenu->addAction(m_pVolumeState->m_pImportAction); pMenu->addSeparator(); pMenu->addAction(m_pVolumeState->m_pSliceAction); pMenu->addAction(m_pVolumeState->m_pSliceSettingsAction); pMenu->addSeparator(); pMenu->addAction(m_pVolumeState->m_pReturnAction); pMenu->addSeparator(); pMenu->addAction(m_pFilterAction); pMenu->addAction(m_pRegionPressureAction); pMenu->addSeparator(); pMenu->addAction(m_pScalarBarAction); pMenu->addAction(m_pScaleGridAction); pMenu->addAction(m_pWellNamesAction); if(m_pControlBar != nullptr && m_pControlBar->getExportAnimationAction() != nullptr) { pMenu->addSeparator(); pMenu->addAction(m_pControlBar->getExportAnimationAction()); } } void nmWxPebiResultViewWidget::onControlOperationFailed( const QString& sError) { // 绑定失败优先显示;输入修改不再提示,也不屏蔽后续真实操作错误。 if(!isImportedVolumeMode() && !m_sBindingError.isEmpty()) { return; } m_sOperationError = sError; refreshStatusBar(); } void nmWxPebiResultViewWidget::onControlOperationSucceeded() { if(isImportedVolumeMode()) { // 底栏的属性切换只同步已应用属性,不覆盖浮动窗口里尚未提交的平面坐标。 if(m_pVolumeState->m_pSliceDialog != nullptr) { m_pVolumeState->m_pSliceDialog->setAppliedProperty( m_pVolumeState->m_pController->getSliceSettings().m_sPropertyId); } m_sOperationError.clear(); synchronizeVolumeActions(); refreshStatusBar(); return; } if(m_sOperationError.isEmpty()) { return; } // 这里只清除普通操作错误;刷新函数仍优先显示绑定失败。 m_sOperationError.clear(); refreshStatusBar(); } void nmWxPebiResultViewWidget::onViewportSizeChanged( int nWidth, int nHeight) { if(m_pVolumeState->m_pController != nullptr) m_pVolumeState->m_pController->setViewportSize(nWidth, nHeight); if(m_pSceneController != nullptr) { // SceneController 缓存尺寸,即使当前尚未创建结果色标也不会丢失。 m_pSceneController->setViewportSize(nWidth, nHeight); } } void nmWxPebiResultViewWidget::onShowFilterSettings() { if(isImportedVolumeMode()) return; nmPebiResultPlaybackController* pPlaybackController = m_pControlBar == nullptr ? nullptr : m_pControlBar->getPlaybackController(); if(m_pSceneController == nullptr || m_pControlBar == nullptr || pPlaybackController == nullptr || !m_pSceneController->hasSnapshot() || pPlaybackController->isPlaying() || m_pControlBar->isExportInProgress() || m_pControlBar->isExternalInteractionLocked()) { return; } nmWxPebiResultFilterSettingsDlg oDialog( m_pSceneController, m_pControlBar, this); connect(&oDialog, SIGNAL(filterSettingsApplied()), this, SLOT(onControlOperationSucceeded())); oDialog.exec(); synchronizeBusinessActions(); } void nmWxPebiResultViewWidget::onRegionPressureStatisticsTriggered() { if(isImportedVolumeMode()) return; if(m_pRegionState == nullptr || m_pControlBar == nullptr) { return; } // 共享 QAction 根据当前状态分别表示:取消绘制、重显结果或开始新绘制。 if(isRegionPressureDrawing()) { clearRegionPressureSelection(true); onControlOperationSucceeded(); return; } if(m_pRegionState->m_eState == nmWxPebiResultRegionState::State_Completed) { showRegionPressureStatisticsDialog(); return; } nmPebiResultPlaybackController* pPlaybackController = m_pControlBar->getPlaybackController(); if(pPlaybackController == nullptr || pPlaybackController->isPlaying() || m_pControlBar->isExportInProgress() || m_pControlBar->isExternalInteractionLocked()) { return; } if(m_pRenderContainer == nullptr || !canProjectRegionOntoXyPlane( m_pRenderContainer->getRenderer())) { onControlOperationFailed(tr( "Switch to Top View or Isometric View before drawing a pressure region.")); return; } if(!startRegionPressureDrawing()) { onControlOperationFailed(tr( "Unable to start region pressure drawing.")); } else { onControlOperationSucceeded(); } } void nmWxPebiResultViewWidget::handleRegionPolygonEvent( vtkObject* pCaller, unsigned long nEventId, void* pClientData, void* pCallData) { Q_UNUSED(pCallData); nmWxPebiResultViewWidget* pView = static_cast(pClientData); if(pView == nullptr || pView->m_pRegionState == nullptr || pView->m_pRegionState->m_eState != nmWxPebiResultRegionState::State_Drawing || pCaller != pView->m_pRegionState->m_pStyle.GetPointer()) { return; } nmWxPebiResultRegionState* pState = pView->m_pRegionState; if(nEventId == nmVTKPlanarPolygonInteractorStyle::PolygonChangedEvent) { nmVTKScene* pScene = pView->m_pRenderContainer == nullptr ? nullptr : pView->m_pRenderContainer->getScene(); if(pScene != nullptr && pState->m_pLayer != nullptr && pScene->getLayer(QString::fromLatin1( PEBI_REGION_PRESSURE_LAYER_ID)) == pState->m_pLayer) { pState->m_pLayer->setPolygon( pState->m_pStyle->getPoints(), pState->m_pStyle->isCompleted()); } return; } // VTK 回调内只复制小量状态;场景切换、对话框和统计计算延后到 Qt 事件循环。 nmWxPebiResultRegionState::PendingEvent ePendingEvent = nmWxPebiResultRegionState::PendingEvent_None; if(nEventId == nmVTKPlanarPolygonInteractorStyle::PolygonCompletedEvent) { ePendingEvent = nmWxPebiResultRegionState::PendingEvent_Completed; pState->m_vecPendingPoints = pState->m_pStyle->getPoints(); } else if(nEventId == nmVTKPlanarPolygonInteractorStyle::PolygonCancelledEvent) { ePendingEvent = nmWxPebiResultRegionState::PendingEvent_Cancelled; } else if(nEventId == nmVTKPlanarPolygonInteractorStyle::PolygonInvalidEvent) { ePendingEvent = nmWxPebiResultRegionState::PendingEvent_Invalid; } else { return; } // 完成或取消优先于尚未处理的无效提示,且每次只排一个 Qt 收尾任务。 if(pState->m_bPendingEventQueued) { if(pState->m_ePendingEvent == nmWxPebiResultRegionState::PendingEvent_Invalid && ePendingEvent != nmWxPebiResultRegionState::PendingEvent_Invalid) { pState->m_ePendingEvent = ePendingEvent; } return; } pState->m_ePendingEvent = ePendingEvent; pState->m_bPendingEventQueued = true; QMetaObject::invokeMethod( pView, "processPendingRegionInteractionEvent", Qt::QueuedConnection); } void nmWxPebiResultViewWidget::processPendingRegionInteractionEvent() { if(m_pRegionState == nullptr) { return; } const nmWxPebiResultRegionState::PendingEvent ePendingEvent = m_pRegionState->m_ePendingEvent; const QVector vecPoints = m_pRegionState->m_vecPendingPoints; m_pRegionState->m_ePendingEvent = nmWxPebiResultRegionState::PendingEvent_None; m_pRegionState->m_bPendingEventQueued = false; m_pRegionState->m_vecPendingPoints.clear(); if(!isRegionPressureDrawing()) { return; } if(ePendingEvent == nmWxPebiResultRegionState::PendingEvent_Invalid) { onControlOperationFailed(tr( "Draw a non-degenerate region with at least three points.")); return; } if(ePendingEvent == nmWxPebiResultRegionState::PendingEvent_Cancelled) { clearRegionPressureSelection(true); onControlOperationSucceeded(); return; } if(ePendingEvent != nmWxPebiResultRegionState::PendingEvent_Completed) { return; } // 绘制期间快照、属性或帧发生变化时,不能把旧坐标区域提交到新结果。 const bool bIdentityMatches = m_pSceneController != nullptr && m_pSceneController->hasSnapshot() && !m_pDisplayedSnapshot.isNull() && m_pDisplayedSnapshot.data() == m_pRegionState->m_pFrozenSnapshot.data() && m_pSceneController->getSnapshotBindingRevision() == m_pRegionState->m_nFrozenBindingRevision && m_pSceneController->getCurrentSnapshotId() == m_pRegionState->m_sFrozenSnapshotId && m_sDisplayedSnapshotId == m_pRegionState->m_sFrozenSnapshotId && m_pSceneController->getCurrentPropertyId() == m_pRegionState->m_sFrozenPropertyId && m_pSceneController->getCurrentFrameIndex() == m_pRegionState->m_nFrozenFrameIndex; if(!bIdentityMatches) { clearRegionPressureSelection(true); onControlOperationFailed(tr( "The displayed result changed while the region was drawn.")); return; } // Calculator::prepare() 事务式建立相交单元和静态权重缓存。 QString sError; if(!m_pRegionState->m_oCalculator.prepare( m_pRegionState->m_pFrozenSnapshot, vecPoints, &sError)) { clearRegionPressureSelection(true); onControlOperationFailed(tr( "Unable to prepare the selected pressure region.")); return; } nmPebiRegionPressureStatistics oStatistics; if(!m_pRegionState->m_oCalculator.calculateFrame( m_pRegionState->m_nFrozenFrameIndex, oStatistics, &sError)) { clearRegionPressureSelection(true); onControlOperationFailed(tr( "Unable to calculate region pressure statistics.")); return; } nmVTKScene* pScene = m_pRenderContainer == nullptr ? nullptr : m_pRenderContainer->getScene(); if(pScene == nullptr || m_pRegionState->m_pLayer == nullptr || pScene->getLayer(QString::fromLatin1( PEBI_REGION_PRESSURE_LAYER_ID)) != m_pRegionState->m_pLayer || !m_pRegionState->m_pLayer->setPolygon( m_pRegionState->m_oCalculator.getPolygonPoints(), true)) { clearRegionPressureSelection(true); onControlOperationFailed(tr( "Unable to preserve the selected pressure region.")); return; } // 几何、计算和轮廓全部成功后才将窗口状态提交为 Completed。 m_pRegionState->m_nRegionBindingRevision = m_pRegionState->m_nFrozenBindingRevision; m_pRegionState->m_sRegionSnapshotId = m_pRegionState->m_sFrozenSnapshotId; m_pRegionState->m_nLastAttemptedFrameIndex = m_pRegionState->m_nFrozenFrameIndex; m_pRegionState->m_bStatisticsCalculated = true; m_pRegionState->m_sStatisticsError.clear(); m_pRegionState->m_oStatistics = oStatistics; m_pRegionState->m_sLastObservedPropertyId = QString::fromLatin1("pressure"); m_pRegionState->m_eState = nmWxPebiResultRegionState::State_Completed; stopRegionPressureDrawing(); onControlOperationSucceeded(); showRegionPressureStatisticsDialog(); synchronizeBusinessActions(); } void nmWxPebiResultViewWidget::onResultDisplayStateChanged() { if(isImportedVolumeMode()) return; if(m_pRegionState != nullptr && m_pSceneController != nullptr && m_pRegionState->m_eState == nmWxPebiResultRegionState::State_Completed) { const QString sCurrentPropertyId = m_pSceneController->getCurrentPropertyId(); if(sCurrentPropertyId == QString::fromLatin1("pressure") && m_pRegionState->m_sLastObservedPropertyId != sCurrentPropertyId) { // 从静态属性切回 pressure 时必须重新评估第 0 帧,即使曾统计过该帧。 m_pRegionState->m_nLastAttemptedFrameIndex = -1; } m_pRegionState->m_sLastObservedPropertyId = sCurrentPropertyId; } synchronizeBusinessActions(); updateRegionPressureStatistics(); } void nmWxPebiResultViewWidget::onCloseRegionPressureStatistics() { if(m_pRegionState == nullptr) { return; } clearRegionPressureSelection(true); onControlOperationSucceeded(); } void nmWxPebiResultViewWidget::onExportAnimationRequested() { if(isImportedVolumeMode()) return; nmPebiResultPlaybackController* pPlaybackController = m_pControlBar == nullptr ? nullptr : m_pControlBar->getPlaybackController(); QString sBackendError; if(m_pSceneController == nullptr || m_pRenderContainer == nullptr || pPlaybackController == nullptr || pPlaybackController->isPlaying() || (m_pControlBar != nullptr && m_pControlBar->isExternalInteractionLocked()) || !m_pSceneController->hasSnapshot() || m_pDisplayedSnapshot.isNull() || !nmFFmpegVideoWriter::isBackendAvailable(&sBackendError)) { if(!sBackendError.isEmpty()) { QMessageBox::warning(this, tr("Export Animation"), sBackendError); } return; } AnimationExportGuard oExportGuard(m_pControlBar); if(!oExportGuard.isAcquired()) { QMessageBox::information( this, tr("Export Animation"), tr("Another result animation export is already in progress.")); return; } const QSharedPointer pFrozenSnapshot = m_pDisplayedSnapshot; const quint64 nFrozenBindingRevision = m_pSceneController->getSnapshotBindingRevision(); const QString sFrozenSnapshotId = m_pSceneController->getCurrentSnapshotId(); const QString sFrozenPropertyId = m_pSceneController->getCurrentPropertyId(); const nmVTKMeshLayer::DisplayMode eFrozenMeshDisplayMode = m_pSceneController->getMeshDisplayMode(); nmPebiResultDisplaySettings oFrozenDisplaySettings; nmPebiResultFilterSettings oFrozenFilterSettings; if(!m_pSceneController->getCurrentDisplaySettings( oFrozenDisplaySettings) || !m_pSceneController->getFilterSettings( oFrozenFilterSettings)) { return; } const int nFrozenFilterFrameIndex = m_pSceneController->getFilterFrameIndex(); const int nFrameCount = pFrozenSnapshot->getPropertyFrameCount( sFrozenPropertyId); if(nFrozenBindingRevision == 0 || sFrozenSnapshotId.isEmpty() || sFrozenPropertyId.isEmpty() || nFrameCount <= 1 || !nmVTKMeshLayer::isValidDisplayMode(eFrozenMeshDisplayMode) || (oFrozenFilterSettings.m_bEnabled && nFrozenFilterFrameIndex < 0) || !m_pSceneController->isPropertyTimeDependent(sFrozenPropertyId)) { return; } QVector vecFrameTimes; vecFrameTimes.reserve(nFrameCount); for(int nFrameIndex = 0; nFrameIndex < nFrameCount; ++nFrameIndex) { double dTimeHours = 0.0; if(!pFrozenSnapshot->getPropertyTimeAt( sFrozenPropertyId, nFrameIndex, dTimeHours)) { QMessageBox::warning( this, tr("Export Animation"), tr("Unable to read the result animation timeline.")); return; } vecFrameTimes.append(dTimeHours); } nmWxPebiResultAnimationExportDlg oSettingsDialog( vecFrameTimes, m_pRenderContainer->getViewportSize(), this); if(oSettingsDialog.exec() != QDialog::Accepted) { return; } // 参数窗口内的文件浏览也会形成嵌套事件循环,开始前重新核对完整显示身份。 if(m_pDisplayedSnapshot.data() != pFrozenSnapshot.data() || m_pSceneController->getSnapshotBindingRevision() != nFrozenBindingRevision || m_pSceneController->getCurrentSnapshotId() != sFrozenSnapshotId || m_pSceneController->getCurrentPropertyId() != sFrozenPropertyId || m_pSceneController->getMeshDisplayMode() != eFrozenMeshDisplayMode) { QMessageBox::warning( this, tr("Export Animation"), tr("The displayed result changed before export started.")); return; } nmPebiResultDisplaySettings oCurrentDisplaySettings; nmPebiResultFilterSettings oCurrentFilterSettings; if(!m_pSceneController->getCurrentDisplaySettings( oCurrentDisplaySettings) || oCurrentDisplaySettings != oFrozenDisplaySettings || !m_pSceneController->getFilterSettings(oCurrentFilterSettings) || oCurrentFilterSettings != oFrozenFilterSettings || m_pSceneController->getFilterFrameIndex() != nFrozenFilterFrameIndex) { QMessageBox::warning( this, tr("Export Animation"), tr("The displayed result changed before export started.")); return; } vtkRenderer* pRenderer = m_pRenderContainer->getRenderer(); if(pRenderer == nullptr || pRenderer->GetActiveCamera() == nullptr) { QMessageBox::warning( this, tr("Export Animation"), tr("Unable to freeze the current result display.")); return; } nmPebiResultAnimationExporter::Request oRequest; oRequest.m_pSnapshot = pFrozenSnapshot; oRequest.m_sPropertyId = sFrozenPropertyId; oRequest.m_oDisplaySettings = oFrozenDisplaySettings; oRequest.m_eMeshDisplayMode = eFrozenMeshDisplayMode; oRequest.m_oFilterSettings = oFrozenFilterSettings; oRequest.m_nFilterFrameIndex = nFrozenFilterFrameIndex; oRequest.m_oExportSettings = oSettingsDialog.getSettings(); oRequest.m_sTargetFilePath = oSettingsDialog.getTargetFilePath(); oRequest.m_bScalarBarVisible = m_pSceneController->isScalarBarVisible(); oRequest.m_bScaleGridVisible = m_pSceneController->isScaleGridVisible(); oRequest.m_bWellNamesVisible = m_pSceneController->areWellNamesVisible(); pRenderer->GetBackground(oRequest.m_dBackground); pRenderer->GetBackground2(oRequest.m_dBackground2); oRequest.m_bGradientBackground = pRenderer->GetGradientBackground() != 0; oRequest.m_pCamera = vtkSmartPointer::New(); oRequest.m_pCamera->DeepCopy(pRenderer->GetActiveCamera()); QProgressDialog oProgressDialog( tr("Exporting result animation..."), tr("Cancel"), 0, oRequest.m_oExportSettings.getOutputFrameCount(), this); oProgressDialog.setWindowTitle(tr("Export Animation")); oProgressDialog.setWindowModality(Qt::ApplicationModal); oProgressDialog.setMinimumDuration(0); oProgressDialog.setAutoClose(false); oProgressDialog.setAutoReset(false); nmPebiResultAnimationExporter oExporter; connect(&oExporter, SIGNAL(progressChanged(int,int)), &oProgressDialog, SLOT(setValue(int))); connect(&oProgressDialog, SIGNAL(canceled()), &oExporter, SLOT(cancel())); oProgressDialog.show(); QString sExportError; const nmPebiResultAnimationExporter::ExportResult eResult = oExporter.exportAnimation(oRequest, &sExportError); oProgressDialog.close(); oExportGuard.release(); if(eResult == nmPebiResultAnimationExporter::Export_Succeeded) { onControlOperationSucceeded(); QMessageBox::information( this, tr("Export Animation"), tr("The result animation was saved to:\n%1") .arg(QDir::toNativeSeparators( oRequest.m_sTargetFilePath))); } else if(eResult == nmPebiResultAnimationExporter::Export_Failed) { QMessageBox::warning( this, tr("Export Animation"), sExportError.isEmpty() ? tr("Unable to export the result animation.") : sExportError); } } void nmWxPebiResultViewWidget::onScalarBarToggled(bool bVisible) { if(isImportedVolumeMode()) { m_pVolumeState->m_pController->setScalarBarVisible(bVisible); return; } if(m_pControlBar != nullptr && m_pControlBar->isExternalInteractionLocked()) { synchronizeBusinessActions(); return; } if(m_pSceneController != nullptr && m_pSceneController->hasSnapshot()) { m_pSceneController->setScalarBarVisible(bVisible); } } void nmWxPebiResultViewWidget::onScaleGridToggled(bool bVisible) { // 比例网格只属于计算结果;外部模型模式不修改挂起结果的显示偏好。 if(isImportedVolumeMode()) return; if(m_pControlBar != nullptr && m_pControlBar->isExternalInteractionLocked()) { synchronizeBusinessActions(); return; } if(m_pSceneController != nullptr && m_pSceneController->hasSnapshot()) { m_pSceneController->setScaleGridVisible(bVisible); } } void nmWxPebiResultViewWidget::onWellNamesToggled(bool bVisible) { if(isImportedVolumeMode()) return; if(m_pControlBar != nullptr && m_pControlBar->isExternalInteractionLocked()) { synchronizeBusinessActions(); return; } if(m_pSceneController != nullptr && m_pSceneController->hasSnapshot()) { m_pSceneController->setWellNamesVisible(bVisible); } } void nmWxPebiResultViewWidget::createUi() { QVBoxLayout* pMainLayout = new QVBoxLayout(this); pMainLayout->setContentsMargins(0, 0, 0, 0); pMainLayout->setSpacing(0); QWidget* pRenderArea = new QWidget(this); QHBoxLayout* pRenderLayout = new QHBoxLayout(pRenderArea); pRenderLayout->setContentsMargins(0, 0, 0, 0); pRenderLayout->setSpacing(0); // 通用容器是本窗口唯一创建 QVTKWidget、Renderer 和 RenderWindow 的对象。 m_pRenderContainer = new nmWxVTKRenderContainerWidget(pRenderArea); m_pRenderContainer->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Expanding); m_pRenderContainer->setViewCommandCapabilities( nmVTKViewCommandController::ViewCommand_ZoomIn | nmVTKViewCommandController::ViewCommand_ZoomOut | nmVTKViewCommandController::ViewCommand_Fit | nmVTKViewCommandController::ViewCommand_Top | nmVTKViewCommandController::ViewCommand_Front | nmVTKViewCommandController::ViewCommand_SetRotationCenter | nmVTKViewCommandController:: ViewCommand_ShowRotationCenterMarker | nmVTKViewCommandController::ViewCommand_SaveImage | nmVTKViewCommandController::ViewCommand_CopyImage | nmVTKViewCommandController::ViewCommand_Print | nmVTKViewCommandController::ViewCommand_PrintPreview); m_pSceneController = new nmPebiResultSceneController(m_pRenderContainer); m_pVolumeState->m_pController = new nmVolumeSceneController(m_pRenderContainer); m_pVolumeState->m_pSliceInteraction = new nmVolumeSliceInteraction(m_pRenderContainer, this); connect(m_pVolumeState->m_pSliceInteraction, SIGNAL(previewChanged(QString,nmVolumeSliceSettings)), this, SLOT(onVolumeSlicePreview(QString,nmVolumeSliceSettings))); connect(m_pVolumeState->m_pSliceInteraction, SIGNAL(applyRequested(QString,nmVolumeSliceSettings)), this, SLOT(onVolumeSliceDragApplied(QString,nmVolumeSliceSettings))); connect(m_pRenderContainer, SIGNAL(rotationCenterPickingChanged(bool)), this, SLOT(synchronizeSliceInteraction())); connect(m_pRenderContainer, SIGNAL(viewportSizeChanged(int,int)), this, SLOT(onViewportSizeChanged(int,int))); const QSize oInitialViewportSize = m_pRenderContainer->getViewportSize(); onViewportSizeChanged( oInitialViewportSize.width(), oInitialViewportSize.height()); m_pToolBar = new QToolBar(pRenderArea); m_pToolBar->setOrientation(Qt::Vertical); m_pToolBar->setMovable(false); m_pToolBar->setFloatable(false); m_pToolBar->setIconSize(QSize(32, 32)); m_pToolBar->setToolButtonStyle(Qt::ToolButtonIconOnly); m_pToolBar->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding); m_pScalarBarAction = new QAction( QIcon(resultViewIconPath("NmColorScale.png")), tr("Color Scale"), this); m_pScaleGridAction = new QAction( QIcon(resultViewIconPath("NmScaleGrid.png")), tr("Scale Grid"), this); m_pWellNamesAction = new QAction( QIcon(resultViewIconPath("NmShowWellNames.png")), tr("Well Names"), this); m_pFilterAction = new QAction( QIcon(resultViewIconPath("NmFilter.png")), tr("Data Filtering..."), this); m_pFilterAction->setToolTip(m_pFilterAction->text()); m_pRegionPressureAction = new QAction( QIcon(resultViewIconPath("NmPolygon.png")), tr("Region Average Pressure"), this); m_pRegionPressureAction->setToolTip( m_pRegionPressureAction->text()); QAction* const pBusinessActions[] = { m_pScalarBarAction, m_pScaleGridAction, m_pWellNamesAction }; for(int nIndex = 0; nIndex < 3; ++nIndex) { pBusinessActions[nIndex]->setCheckable(true); pBusinessActions[nIndex]->setToolTip( pBusinessActions[nIndex]->text()); } nmVTKViewCommandController* pCommandController = m_pRenderContainer->getViewCommandController(); if(pCommandController != nullptr) { QAction* pZoomOutAction = pCommandController->getAction( nmVTKViewCommandController::ViewCommand_ZoomOut); QAction* pFitAction = pCommandController->getAction( nmVTKViewCommandController::ViewCommand_Fit); QAction* pZoomInAction = pCommandController->getAction( nmVTKViewCommandController::ViewCommand_ZoomIn); QAction* pTopAction = pCommandController->getAction( nmVTKViewCommandController::ViewCommand_Top); QAction* pFrontAction = pCommandController->getAction( nmVTKViewCommandController::ViewCommand_Front); QAction* pPickCenterAction = pCommandController->getAction( nmVTKViewCommandController::ViewCommand_SetRotationCenter); QAction* pCopyAction = pCommandController->getAction( nmVTKViewCommandController::ViewCommand_CopyImage); if(pZoomOutAction != nullptr) m_pToolBar->addAction(pZoomOutAction); if(pFitAction != nullptr) m_pToolBar->addAction(pFitAction); if(pZoomInAction != nullptr) m_pToolBar->addAction(pZoomInAction); m_pToolBar->addSeparator(); if(pTopAction != nullptr) m_pToolBar->addAction(pTopAction); if(pFrontAction != nullptr) m_pToolBar->addAction(pFrontAction); if(pPickCenterAction != nullptr) m_pToolBar->addAction(pPickCenterAction); m_pToolBar->addSeparator(); m_pToolBar->addAction(m_pFilterAction); m_pToolBar->addAction(m_pRegionPressureAction); for(int nIndex = 0; nIndex < 3; ++nIndex) { m_pToolBar->addAction(pBusinessActions[nIndex]); } m_pToolBar->addSeparator(); if(pCopyAction != nullptr) m_pToolBar->addAction(pCopyAction); } // 只增加一个常驻按钮。小图标直接绘制,避免部署时丢失额外资源文件。 QPixmap oSliceIcon(32, 32); oSliceIcon.fill(Qt::transparent); { QPainter oPainter(&oSliceIcon); oPainter.setRenderHint(QPainter::Antialiasing); oPainter.setPen(QPen(QColor(65, 85, 105), 1.5)); oPainter.drawRect(7, 5, 18, 22); oPainter.setBrush(QColor(60, 155, 215, 160)); QPolygon oPlane; oPlane << QPoint(3, 20) << QPoint(20, 11) << QPoint(29, 14) << QPoint(12, 24); oPainter.drawPolygon(oPlane); } m_pVolumeState->m_pButton = new QToolButton(m_pToolBar); m_pVolumeState->m_pButton->setObjectName(QLatin1String("volumeSliceButton")); m_pVolumeState->m_pButton->setIcon(QIcon(oSliceIcon)); m_pVolumeState->m_pButton->setIconSize(m_pToolBar->iconSize()); m_pVolumeState->m_pButton->setText(tr("Slice")); m_pVolumeState->m_pButton->setToolTip(tr("Slice")); m_pVolumeState->m_pButton->setAutoRaise(true); m_pVolumeState->m_pButton->setPopupMode(QToolButton::InstantPopup); QMenu* pSliceMenu = new QMenu(m_pVolumeState->m_pButton); m_pVolumeState->m_pImportAction = new QAction(tr("Import 3D Model..."), this); m_pVolumeState->m_pImportAction->setObjectName(QLatin1String("importVolumeAction")); m_pVolumeState->m_pReturnAction = new QAction(tr("Return to Calculation Result"), this); m_pVolumeState->m_pReturnAction->setObjectName(QLatin1String("returnCalculationAction")); m_pVolumeState->m_pSliceAction = new QAction(tr("Show Slice"), this); m_pVolumeState->m_pSliceAction->setObjectName(QLatin1String("showVolumeSliceAction")); m_pVolumeState->m_pSliceAction->setCheckable(true); m_pVolumeState->m_pSliceSettingsAction = new QAction(tr("Slice Settings..."), this); m_pVolumeState->m_pSliceSettingsAction->setObjectName(QLatin1String("volumeSliceSettingsAction")); pSliceMenu->addAction(m_pVolumeState->m_pImportAction); pSliceMenu->addSeparator(); pSliceMenu->addAction(m_pVolumeState->m_pSliceAction); pSliceMenu->addAction(m_pVolumeState->m_pSliceSettingsAction); pSliceMenu->addSeparator(); pSliceMenu->addAction(m_pVolumeState->m_pReturnAction); m_pVolumeState->m_pButton->setMenu(pSliceMenu); m_pToolBar->addWidget(m_pVolumeState->m_pButton); connect(m_pVolumeState->m_pImportAction, SIGNAL(triggered()), this, SLOT(onImportVolumeRequested())); connect(m_pVolumeState->m_pReturnAction, SIGNAL(triggered()), this, SLOT(onReturnToCalculationResult())); connect(m_pVolumeState->m_pSliceAction, SIGNAL(toggled(bool)), this, SLOT(onVolumeSliceToggled(bool))); connect(m_pVolumeState->m_pSliceSettingsAction, SIGNAL(triggered()), this, SLOT(onShowVolumeSliceSettings())); connect(m_pScalarBarAction, SIGNAL(toggled(bool)), this, SLOT(onScalarBarToggled(bool))); connect(m_pScaleGridAction, SIGNAL(toggled(bool)), this, SLOT(onScaleGridToggled(bool))); connect(m_pWellNamesAction, SIGNAL(toggled(bool)), this, SLOT(onWellNamesToggled(bool))); connect(m_pFilterAction, SIGNAL(triggered()), this, SLOT(onShowFilterSettings())); connect(m_pRegionPressureAction, SIGNAL(triggered()), this, SLOT(onRegionPressureStatisticsTriggered())); connect(m_pRenderContainer, SIGNAL(contextMenuAboutToShow(QMenu*)), this, SLOT(appendContextMenuActions(QMenu*))); pRenderLayout->addWidget(m_pRenderContainer, 1); pRenderLayout->addWidget(m_pToolBar); pMainLayout->addWidget(pRenderArea, 1); m_pStatusLabel = new QLabel(this); m_pStatusLabel->setWordWrap(true); m_pStatusLabel->setContentsMargins(8, 4, 8, 4); m_pStatusLabel->hide(); pMainLayout->addWidget(m_pStatusLabel); m_pControlBar = new nmWxPebiResultControlBar( m_pSceneController, this); connect(m_pControlBar, SIGNAL(operationFailed(QString)), this, SLOT(onControlOperationFailed(QString))); connect(m_pControlBar, SIGNAL(operationSucceeded()), this, SLOT(onControlOperationSucceeded())); connect(m_pControlBar, SIGNAL(exportAnimationRequested()), this, SLOT(onExportAnimationRequested())); connect(m_pControlBar, SIGNAL(exportInProgressChanged(bool)), this, SLOT(synchronizeBusinessActions())); if(m_pControlBar->getPlaybackController() != nullptr) { connect(m_pControlBar->getPlaybackController(), SIGNAL(stateChanged()), this, SLOT(onResultDisplayStateChanged())); } pMainLayout->addWidget(m_pControlBar); synchronizeBusinessActions(); m_pRenderContainer->showEmptyState(tr("No result data.")); } void nmWxPebiResultViewWidget::synchronizeOnce() { if(isImportedVolumeMode()) { // 正式结果变化不能抢占正在查看的外部模型;返回时读取 Manager 最新快照。 m_pVolumeState->m_bPendingSync = true; return; } if(m_pDataManager.isNull()) { if(m_bObservedStateValid && m_pObservedSnapshot.isNull()) { return; } clearSceneAndControls(tr("No result data.")); rememberObservedState( QSharedPointer(), QString(), false); return; } const QSharedPointer pSnapshot = m_pDataManager->getPebiResultSnapshot(); const QString sSnapshotId = pSnapshot.isNull() ? QString() : pSnapshot->getSnapshotId(); const bool bCurrent = !pSnapshot.isNull() && m_pDataManager->isPebiResultSnapshotCurrent(); const bool bSameIdentity = m_bObservedStateValid && m_pObservedSnapshot.data() == pSnapshot.data() && m_sObservedSnapshotId == sSnapshotId; const bool bSameState = bSameIdentity && m_bObservedSnapshotCurrent == bCurrent; if(bSameState) { return; } const bool bBecameStale = bSameIdentity && m_bObservedSnapshotCurrent && !bCurrent; m_sOperationError.clear(); if(pSnapshot.isNull()) { clearSceneAndControls(tr("No result data.")); rememberObservedState(pSnapshot, sSnapshotId, false); return; } if(!bSameIdentity) { // 候选提交前停止本窗口 Timer;失败时旧 Scene 和最后压力帧记忆仍保留。 m_pControlBar->stopPlayback(); QString sError; const bool bBound = m_pSceneController->setSnapshot( pSnapshot, &sError); rememberObservedState(pSnapshot, sSnapshotId, bCurrent); if(bBound) { m_pDisplayedSnapshot = pSnapshot; m_sDisplayedSnapshotId = sSnapshotId; m_sBindingError.clear(); // 只有新快照已经成功发布后才清旧区域;失败回滚必须完整保留旧统计。 clearRegionPressureSelection(true); // 新成功绑定序号会让 Playback 和 ControlBar 同时重置各自缓存。 m_pControlBar->synchronizeFromScene(); synchronizeBusinessActions(); refreshStatusBar(); return; } m_sBindingError = sError.isEmpty() ? tr("Unable to display the latest result.") : tr("Unable to display the latest result: %1").arg(sError); if(!m_pSceneController->hasSnapshot()) { m_pDisplayedSnapshot.clear(); m_sDisplayedSnapshotId.clear(); } m_pControlBar->synchronizeFromScene(); synchronizeBusinessActions(); if(m_pSceneController->hasSnapshot()) { // 旧画面保持 Ready;这里只显示红色非阻塞提示,不能追加 Render。 refreshStatusBar(); } else { m_pStatusLabel->hide(); m_pRenderContainer->showErrorState(m_sBindingError); } return; } rememberObservedState(pSnapshot, sSnapshotId, bCurrent); if(bBecameStale) { // 只在“当前 -> 过期”的边沿暂停一次;用户之后可以手动重新播放旧结果。 m_pControlBar->stopPlayback(); } refreshStatusBar(); } void nmWxPebiResultViewWidget::clearSceneAndControls( const QString& sEmptyMessage) { if(m_pControlBar != nullptr) { m_pControlBar->stopPlayback(); } clearRegionPressureSelection(false); if(m_pSceneController != nullptr) { m_pSceneController->clear(); } m_pObservedSnapshot.clear(); m_sObservedSnapshotId.clear(); m_bObservedSnapshotCurrent = false; m_bObservedStateValid = false; m_pDisplayedSnapshot.clear(); m_sDisplayedSnapshotId.clear(); m_sBindingError.clear(); m_sOperationError.clear(); if(m_pControlBar != nullptr) { m_pControlBar->synchronizeFromScene(); } synchronizeBusinessActions(); if(m_pStatusLabel != nullptr) { m_pStatusLabel->hide(); } if(m_pRenderContainer != nullptr) { m_pRenderContainer->showEmptyState(sEmptyMessage); } } bool nmWxPebiResultViewWidget::startRegionPressureDrawing() { nmPebiResultPlaybackController* pPlaybackController = m_pControlBar == nullptr ? nullptr : m_pControlBar->getPlaybackController(); if(m_pRegionState == nullptr || m_pSceneController == nullptr || m_pRenderContainer == nullptr || m_pControlBar == nullptr || pPlaybackController == nullptr || m_pRegionState->m_eState != nmWxPebiResultRegionState::State_Idle || !m_pSceneController->hasSnapshot() || m_pDisplayedSnapshot.isNull() || m_pSceneController->getCurrentPropertyId() != QString::fromLatin1("pressure") || m_pSceneController->getCurrentFrameIndex() < 0 || pPlaybackController->isPlaying() || m_pControlBar->isExportInProgress() || m_pControlBar->isExternalInteractionLocked() || !canProjectRegionOntoXyPlane( m_pRenderContainer->getRenderer()) || !ensureRegionPressureLayer()) { return false; } // 冻结绘制起点的完整显示身份,完成时会逐项复核。 m_pRegionState->m_pFrozenSnapshot = m_pDisplayedSnapshot; m_pRegionState->m_nFrozenBindingRevision = m_pSceneController->getSnapshotBindingRevision(); m_pRegionState->m_sFrozenSnapshotId = m_pSceneController->getCurrentSnapshotId(); m_pRegionState->m_sFrozenPropertyId = m_pSceneController->getCurrentPropertyId(); m_pRegionState->m_nFrozenFrameIndex = m_pSceneController->getCurrentFrameIndex(); if(m_pRegionState->m_nFrozenBindingRevision == 0 || m_pRegionState->m_sFrozenSnapshotId.isEmpty() || m_pRegionState->m_sFrozenSnapshotId != m_sDisplayedSnapshotId) { m_pRegionState->m_pFrozenSnapshot.clear(); return false; } // 观察者只在本次绘制事务内有效,退出时必须在销毁回调前全部解除。 m_pRegionState->m_pStyle = vtkSmartPointer::New(); m_pRegionState->m_pCallback = vtkSmartPointer::New(); m_pRegionState->m_pCallback->SetClientData(this); m_pRegionState->m_pCallback->SetCallback( &nmWxPebiResultViewWidget::handleRegionPolygonEvent); m_pRegionState->m_nChangedObserverTag = m_pRegionState->m_pStyle->AddObserver( nmVTKPlanarPolygonInteractorStyle::PolygonChangedEvent, m_pRegionState->m_pCallback); m_pRegionState->m_nCompletedObserverTag = m_pRegionState->m_pStyle->AddObserver( nmVTKPlanarPolygonInteractorStyle::PolygonCompletedEvent, m_pRegionState->m_pCallback); m_pRegionState->m_nCancelledObserverTag = m_pRegionState->m_pStyle->AddObserver( nmVTKPlanarPolygonInteractorStyle::PolygonCancelledEvent, m_pRegionState->m_pCallback); m_pRegionState->m_nInvalidObserverTag = m_pRegionState->m_pStyle->AddObserver( nmVTKPlanarPolygonInteractorStyle::PolygonInvalidEvent, m_pRegionState->m_pCallback); if(!m_pRenderContainer->installInteractionStyle( m_pRegionState->m_pStyle)) { stopRegionPressureDrawing(); return false; } m_pRegionState->m_oCalculator.clear(); m_pRegionState->m_pLayer->clear(); if(m_pRegionState->m_pDialog != nullptr) { m_pRegionState->m_pDialog->hide(); } // 交互样式安装成功后再锁定底栏,避免失败路径遗留禁用控件。 m_pRegionState->m_eState = nmWxPebiResultRegionState::State_Drawing; m_pControlBar->setExternalInteractionLocked(true); synchronizeBusinessActions(); m_pRenderContainer->renderView(); return true; } void nmWxPebiResultViewWidget::stopRegionPressureDrawing() { if(m_pRegionState == nullptr) { return; } // 先移除 VTK 观察者并清空 ClientData,再恢复默认相机交互。 if(m_pRegionState->m_pStyle != nullptr) { if(m_pRegionState->m_nChangedObserverTag != 0) { m_pRegionState->m_pStyle->RemoveObserver( m_pRegionState->m_nChangedObserverTag); } if(m_pRegionState->m_nCompletedObserverTag != 0) { m_pRegionState->m_pStyle->RemoveObserver( m_pRegionState->m_nCompletedObserverTag); } if(m_pRegionState->m_nCancelledObserverTag != 0) { m_pRegionState->m_pStyle->RemoveObserver( m_pRegionState->m_nCancelledObserverTag); } if(m_pRegionState->m_nInvalidObserverTag != 0) { m_pRegionState->m_pStyle->RemoveObserver( m_pRegionState->m_nInvalidObserverTag); } } if(m_pRegionState->m_pCallback != nullptr) { m_pRegionState->m_pCallback->SetClientData(nullptr); } m_pRegionState->m_nChangedObserverTag = 0; m_pRegionState->m_nCompletedObserverTag = 0; m_pRegionState->m_nCancelledObserverTag = 0; m_pRegionState->m_nInvalidObserverTag = 0; if(m_pRenderContainer != nullptr && m_pRegionState->m_pStyle != nullptr) { m_pRenderContainer->restoreDefaultInteraction(); } m_pRegionState->m_pStyle = nullptr; m_pRegionState->m_pCallback = nullptr; m_pRegionState->m_ePendingEvent = nmWxPebiResultRegionState::PendingEvent_None; m_pRegionState->m_bPendingEventQueued = false; m_pRegionState->m_vecPendingPoints.clear(); m_pRegionState->m_pFrozenSnapshot.clear(); m_pRegionState->m_nFrozenBindingRevision = 0; m_pRegionState->m_sFrozenSnapshotId.clear(); m_pRegionState->m_sFrozenPropertyId.clear(); m_pRegionState->m_nFrozenFrameIndex = -1; if(m_pControlBar != nullptr) { m_pControlBar->setExternalInteractionLocked(false); } } void nmWxPebiResultViewWidget::clearRegionPressureSelection( bool bRenderView) { if(m_pRegionState == nullptr) { return; } nmVTKScene* pScene = m_pRenderContainer == nullptr ? nullptr : m_pRenderContainer->getScene(); // 裸指针只在 Scene 仍持有同一图层时可用,失败回滚后不访问旧对象。 const bool bLayerIsCurrent = pScene != nullptr && m_pRegionState->m_pLayer != nullptr && pScene->getLayer(QString::fromLatin1( PEBI_REGION_PRESSURE_LAYER_ID)) == m_pRegionState->m_pLayer; const bool bHadVisibleRegion = m_pRegionState->m_eState != nmWxPebiResultRegionState::State_Idle || (bLayerIsCurrent && !m_pRegionState->m_pLayer->getPoints().isEmpty()); stopRegionPressureDrawing(); if(bLayerIsCurrent) { m_pRegionState->m_pLayer->clear(); } else { m_pRegionState->m_pLayer = nullptr; } m_pRegionState->m_oCalculator.clear(); m_pRegionState->m_eState = nmWxPebiResultRegionState::State_Idle; m_pRegionState->m_nRegionBindingRevision = 0; m_pRegionState->m_sRegionSnapshotId.clear(); m_pRegionState->m_sLastObservedPropertyId.clear(); m_pRegionState->m_nLastAttemptedFrameIndex = -1; m_pRegionState->m_bStatisticsCalculated = false; m_pRegionState->m_sStatisticsError.clear(); m_pRegionState->m_oStatistics.clear(); if(m_pRegionState->m_pDialog != nullptr) { m_pRegionState->m_pDialog->hide(); } synchronizeBusinessActions(); if(bRenderView && bHadVisibleRegion && m_pRenderContainer != nullptr) { m_pRenderContainer->renderView(); } } void nmWxPebiResultViewWidget::destroyRegionPressureState() { if(m_pRegionState == nullptr) { return; } clearRegionPressureSelection(false); nmVTKScene* pScene = m_pRenderContainer == nullptr ? nullptr : m_pRenderContainer->getScene(); if(pScene != nullptr && m_pRegionState->m_pLayer != nullptr && pScene->getLayer(QString::fromLatin1( PEBI_REGION_PRESSURE_LAYER_ID)) == m_pRegionState->m_pLayer) { pScene->removeLayer(QString::fromLatin1( PEBI_REGION_PRESSURE_LAYER_ID)); } m_pRegionState->m_pLayer = nullptr; if(m_pRegionState->m_pDialog != nullptr) { delete m_pRegionState->m_pDialog; m_pRegionState->m_pDialog = nullptr; } delete m_pRegionState; m_pRegionState = nullptr; } bool nmWxPebiResultViewWidget::ensureRegionPressureLayer() { if(m_pRegionState == nullptr || m_pRenderContainer == nullptr || m_pRenderContainer->getScene() == nullptr) { return false; } nmVTKScene* pScene = m_pRenderContainer->getScene(); nmVTKRenderLayer* pExisting = pScene->getLayer( QString::fromLatin1(PEBI_REGION_PRESSURE_LAYER_ID)); if(pExisting != nullptr) { return m_pRegionState->m_pLayer != nullptr && pExisting == m_pRegionState->m_pLayer; } m_pRegionState->m_pLayer = nullptr; nmVTKPlanarPolygonLayer* pCandidate = new nmVTKPlanarPolygonLayer(QString::fromLatin1( PEBI_REGION_PRESSURE_LAYER_ID)); if(!pScene->addLayer(pCandidate)) { delete pCandidate; return false; } m_pRegionState->m_pLayer = pCandidate; return true; } bool nmWxPebiResultViewWidget::isRegionPressureDrawing() const { return m_pRegionState != nullptr && m_pRegionState->m_eState == nmWxPebiResultRegionState::State_Drawing; } void nmWxPebiResultViewWidget::updateRegionPressureStatistics() { // 静态属性下冻结最后一帧结果;只有显示 pressure 时才跟随帧变化重算。 if(m_pRegionState == nullptr || m_pSceneController == nullptr || m_pRegionState->m_eState != nmWxPebiResultRegionState::State_Completed || !m_pRegionState->m_oCalculator.isPrepared() || !m_pSceneController->hasSnapshot() || m_pSceneController->getSnapshotBindingRevision() != m_pRegionState->m_nRegionBindingRevision || m_pSceneController->getCurrentSnapshotId() != m_pRegionState->m_sRegionSnapshotId || m_pSceneController->getCurrentPropertyId() != QString::fromLatin1("pressure")) { return; } const int nFrameIndex = m_pSceneController->getCurrentFrameIndex(); if(nFrameIndex < 0 || nFrameIndex == m_pRegionState->m_nLastAttemptedFrameIndex) { return; } // 先登记尝试帧,保证一个状态信号周期内同一帧最多进行一轮重计算。 m_pRegionState->m_nLastAttemptedFrameIndex = nFrameIndex; nmPebiRegionPressureStatistics oStatistics; QString sError; m_pRegionState->m_bStatisticsCalculated = m_pRegionState->m_oCalculator.calculateFrame( nFrameIndex, oStatistics, &sError); m_pRegionState->m_sStatisticsError = sError; if(m_pRegionState->m_bStatisticsCalculated) { m_pRegionState->m_oStatistics = oStatistics; } refreshRegionPressureStatisticsDialog(); } void nmWxPebiResultViewWidget::ensureRegionPressureStatisticsDialog() { if(m_pRegionState == nullptr || m_pRegionState->m_pDialog != nullptr) { return; } iDlgBase* pDialog = new iDlgBase(this); pDialog->setWindowTitle(tr("Region Average Pressure")); pDialog->setWindowModality(Qt::NonModal); pDialog->setModal(false); pDialog->setAttribute(Qt::WA_DeleteOnClose, false); pDialog->setWindowFlags( pDialog->windowFlags() & ~Qt::WindowContextHelpButtonHint); QVBoxLayout* pMainLayout = new QVBoxLayout(pDialog); pMainLayout->setContentsMargins(12, 10, 12, 10); pMainLayout->setSpacing(8); QGroupBox* pStatisticsGroup = new QGroupBox( tr("Statistics"), pDialog); QGridLayout* pStatisticsLayout = new QGridLayout(pStatisticsGroup); pStatisticsLayout->setContentsMargins(8, 15, 8, 10); pStatisticsLayout->setHorizontalSpacing(6); pStatisticsLayout->setVerticalSpacing(6); pStatisticsLayout->setColumnStretch(0, 0); pStatisticsLayout->setColumnStretch(1, 8); pStatisticsLayout->setColumnStretch(2, 5); m_pRegionState->m_pAveragePressureUnitCombo = createRegionUnitCombo(pStatisticsGroup, QString::fromLatin1("MPa")); m_pRegionState->m_pMinimumPressureUnitCombo = createRegionUnitCombo(pStatisticsGroup, QString::fromLatin1("MPa")); m_pRegionState->m_pMaximumPressureUnitCombo = createRegionUnitCombo(pStatisticsGroup, QString::fromLatin1("MPa")); m_pRegionState->m_pPressureTimeUnitCombo = createRegionUnitCombo(pStatisticsGroup, QString::fromLatin1("h")); int nRow = 0; m_pRegionState->m_pAveragePressureValue = addRegionStatisticsRow( pStatisticsLayout, nRow++, tr("Region average pressure"), pStatisticsGroup, m_pRegionState->m_pAveragePressureUnitCombo); m_pRegionState->m_pAveragePressureValue->setToolTip(tr( "Weighted by cell area, thickness and porosity.")); m_pRegionState->m_pMinimumPressureValue = addRegionStatisticsRow( pStatisticsLayout, nRow++, tr("Minimum pressure"), pStatisticsGroup, m_pRegionState->m_pMinimumPressureUnitCombo); m_pRegionState->m_pMaximumPressureValue = addRegionStatisticsRow( pStatisticsLayout, nRow++, tr("Maximum pressure"), pStatisticsGroup, m_pRegionState->m_pMaximumPressureUnitCombo); m_pRegionState->m_pTimeValue = addRegionStatisticsRow( pStatisticsLayout, nRow++, tr("Pressure time"), pStatisticsGroup, m_pRegionState->m_pPressureTimeUnitCombo); m_pRegionState->m_pSelectedCellValue = addRegionStatisticsRow( pStatisticsLayout, nRow++, tr("Selected cells"), pStatisticsGroup); m_pRegionState->m_pWarningLabel = new QLabel(pStatisticsGroup); m_pRegionState->m_pWarningLabel->setWordWrap(true); m_pRegionState->m_pWarningLabel->setStyleSheet( "QLabel { color: #a32626; }"); m_pRegionState->m_pWarningLabel->hide(); pStatisticsLayout->addWidget( m_pRegionState->m_pWarningLabel, nRow++, 0, 1, 3); pMainLayout->addWidget(pStatisticsGroup); QHBoxLayout* pButtonLayout = new QHBoxLayout; pButtonLayout->setContentsMargins(0, 0, 0, 0); pButtonLayout->setSpacing(6); pButtonLayout->addStretch(1); QPushButton* pCloseButton = new QPushButton(tr("Close"), pDialog); const QSize oFooterIconSize(16, 16); pCloseButton->setIcon(QIcon(resultViewIconPath("Close.png"))); pCloseButton->setIconSize(oFooterIconSize); pCloseButton->setMinimumWidth(72); pCloseButton->setDefault(true); pButtonLayout->addWidget(pCloseButton); pMainLayout->addLayout(pButtonLayout); connect(pCloseButton, SIGNAL(clicked()), this, SLOT(onCloseRegionPressureStatistics())); connect(pDialog, SIGNAL(rejected()), this, SLOT(onCloseRegionPressureStatistics())); m_pRegionState->m_pDialog = pDialog; pDialog->setMinimumWidth(460); pDialog->adjustSize(); } void nmWxPebiResultViewWidget::refreshRegionPressureStatisticsDialog() { if(m_pRegionState == nullptr || m_pRegionState->m_pDialog == nullptr) { return; } const QString sUnavailable = QString::fromLatin1("--"); if(!m_pRegionState->m_bStatisticsCalculated) { QLineEdit* const pValues[] = { m_pRegionState->m_pAveragePressureValue, m_pRegionState->m_pMinimumPressureValue, m_pRegionState->m_pMaximumPressureValue, m_pRegionState->m_pTimeValue, m_pRegionState->m_pSelectedCellValue }; const int nValueCount = sizeof(pValues) / sizeof(pValues[0]); for(int nIndex = 0; nIndex < nValueCount; ++nIndex) { pValues[nIndex]->setText(sUnavailable); } m_pRegionState->m_pWarningLabel->setText( tr("Unable to calculate region average pressure.")); m_pRegionState->m_pWarningLabel->show(); return; } const nmPebiRegionPressureStatistics& oStatistics = m_pRegionState->m_oStatistics; m_pRegionState->m_pTimeValue->setText( formatRegionValue(oStatistics.m_dPressureTimeHours)); m_pRegionState->m_pSelectedCellValue->setText( QString::number(static_cast( oStatistics.m_nIntersectingCellCount))); if(oStatistics.m_bAvailable) { m_pRegionState->m_pAveragePressureValue->setText( formatRegionValue( oStatistics.m_dPoreVolumeWeightedPressureMpa)); m_pRegionState->m_pMinimumPressureValue->setText( formatRegionValue(oStatistics.m_dMinimumPressureMpa)); m_pRegionState->m_pMaximumPressureValue->setText( formatRegionValue(oStatistics.m_dMaximumPressureMpa)); } else { m_pRegionState->m_pAveragePressureValue->setText(sUnavailable); m_pRegionState->m_pMinimumPressureValue->setText(sUnavailable); m_pRegionState->m_pMaximumPressureValue->setText(sUnavailable); } if(oStatistics.m_nIntersectingCellCount == 0) { m_pRegionState->m_pWarningLabel->setText( tr("No cells intersect the selected region.")); m_pRegionState->m_pWarningLabel->show(); } else if(!oStatistics.m_bAvailable) { m_pRegionState->m_pWarningLabel->setText(tr( "No valid cells are available for region average pressure.")); m_pRegionState->m_pWarningLabel->show(); } else if(oStatistics.m_nIgnoredCellCount > 0) { m_pRegionState->m_pWarningLabel->setText(tr( "%1 selected cells were excluded because their data is invalid.").arg( static_cast( oStatistics.m_nIgnoredCellCount))); m_pRegionState->m_pWarningLabel->show(); } else { m_pRegionState->m_pWarningLabel->clear(); m_pRegionState->m_pWarningLabel->hide(); } } void nmWxPebiResultViewWidget::showRegionPressureStatisticsDialog() { nmVTKScene* pScene = m_pRenderContainer == nullptr ? nullptr : m_pRenderContainer->getScene(); if(m_pRegionState == nullptr || m_pSceneController == nullptr || m_pRegionState->m_eState != nmWxPebiResultRegionState::State_Completed || !m_pRegionState->m_oCalculator.isPrepared() || !m_pSceneController->hasSnapshot() || m_pSceneController->getSnapshotBindingRevision() != m_pRegionState->m_nRegionBindingRevision || m_pSceneController->getCurrentSnapshotId() != m_pRegionState->m_sRegionSnapshotId || pScene == nullptr || m_pRegionState->m_pLayer == nullptr || pScene->getLayer(QString::fromLatin1( PEBI_REGION_PRESSURE_LAYER_ID)) != m_pRegionState->m_pLayer) { return; } ensureRegionPressureStatisticsDialog(); synchronizeBusinessActions(); refreshRegionPressureStatisticsDialog(); if(m_pRegionState->m_pDialog != nullptr) { m_pRegionState->m_pDialog->show(); m_pRegionState->m_pDialog->raise(); m_pRegionState->m_pDialog->activateWindow(); } } void nmWxPebiResultViewWidget::rememberObservedState( const QSharedPointer& pSnapshot, const QString& sSnapshotId, bool bCurrent) { m_pObservedSnapshot = pSnapshot; m_sObservedSnapshotId = sSnapshotId; m_bObservedSnapshotCurrent = bCurrent; m_bObservedStateValid = true; } void nmWxPebiResultViewWidget::synchronizeBusinessActions() { synchronizeVolumeActions(); if(isImportedVolumeMode()) return; if(m_pSceneController == nullptr) { return; } setActionCheckedWithoutSignal( m_pScalarBarAction, m_pSceneController->isScalarBarVisible()); setActionCheckedWithoutSignal( m_pScaleGridAction, m_pSceneController->isScaleGridVisible()); setActionCheckedWithoutSignal( m_pWellNamesAction, m_pSceneController->areWellNamesVisible()); const bool bEnabled = m_pSceneController->hasSnapshot(); nmPebiResultPlaybackController* pPlaybackController = m_pControlBar == nullptr ? nullptr : m_pControlBar->getPlaybackController(); const bool bPlaying = pPlaybackController != nullptr && pPlaybackController->isPlaying(); const bool bExporting = m_pControlBar != nullptr && m_pControlBar->isExportInProgress(); const bool bExternallyLocked = m_pControlBar != nullptr && m_pControlBar->isExternalInteractionLocked(); const bool bDrawing = isRegionPressureDrawing(); nmVTKScene* pScene = m_pRenderContainer == nullptr ? nullptr : m_pRenderContainer->getScene(); const bool bCompleted = m_pRegionState != nullptr && m_pRegionState->m_eState == nmWxPebiResultRegionState::State_Completed && m_pRegionState->m_oCalculator.isPrepared() && bEnabled && m_pSceneController->getSnapshotBindingRevision() == m_pRegionState->m_nRegionBindingRevision && m_pSceneController->getCurrentSnapshotId() == m_pRegionState->m_sRegionSnapshotId && pScene != nullptr && m_pRegionState->m_pLayer != nullptr && pScene->getLayer(QString::fromLatin1( PEBI_REGION_PRESSURE_LAYER_ID)) == m_pRegionState->m_pLayer; const bool bBusinessVisibilityEnabled = bEnabled && !bDrawing; m_pScalarBarAction->setEnabled(bBusinessVisibilityEnabled); m_pScaleGridAction->setEnabled(bBusinessVisibilityEnabled); m_pWellNamesAction->setEnabled(bBusinessVisibilityEnabled); const bool bFilterEnabled = bEnabled && pPlaybackController != nullptr && !bPlaying && !bExporting && !bExternallyLocked && !bDrawing; if(m_pFilterAction != nullptr) { m_pFilterAction->setEnabled(bFilterEnabled); } if(m_pRegionPressureAction != nullptr) { const bool bCanStart = bEnabled && m_pSceneController->getCurrentPropertyId() == QString::fromLatin1("pressure"); const bool bRegionEnabled = bDrawing || bCompleted || (!bExternallyLocked && !bPlaying && !bExporting && bCanStart); m_pRegionPressureAction->setEnabled(bRegionEnabled); const QString sActionText = bDrawing ? tr("Cancel Region Drawing") : tr("Region Average Pressure"); m_pRegionPressureAction->setText(sActionText); m_pRegionPressureAction->setToolTip(sActionText); } } void nmWxPebiResultViewWidget::refreshStatusBar() { if(isImportedVolumeMode()) { const bool bError = !m_sOperationError.isEmpty(); const QString sStatus = bError ? m_sOperationError : (m_pVolumeState->m_pController->isSliceEmpty() ? tr("The slice plane does not intersect the model.") : QString()); m_pStatusLabel->setStyleSheet(bError ? "QLabel { color: #a32626; background: #fdecec; }" : "QLabel { color: #725514; background: #fff7df; }"); m_pStatusLabel->setText(sStatus); m_pStatusLabel->setVisible(!sStatus.isEmpty()); return; } if(m_pStatusLabel == nullptr || m_pSceneController == nullptr || !m_pSceneController->hasSnapshot()) { if(m_pStatusLabel != nullptr) { m_pStatusLabel->hide(); } return; } if(!m_sBindingError.isEmpty()) { m_pStatusLabel->setStyleSheet( "QLabel { color: #a32626; background: #fdecec; } "); m_pStatusLabel->setText(m_sBindingError); m_pStatusLabel->show(); return; } if(!m_sOperationError.isEmpty()) { m_pStatusLabel->setStyleSheet( "QLabel { color: #a32626; background: #fdecec; } "); m_pStatusLabel->setText(m_sOperationError); m_pStatusLabel->show(); return; } m_pStatusLabel->hide(); } void nmWxPebiResultViewWidget::disconnectDataManager() { nmDataAnalyzeManager* pDataManager = m_pDataManager.data(); if(pDataManager != nullptr) { disconnect(pDataManager, SIGNAL(sigPebiResultStateChanged()), this, SLOT(onPebiResultStateChanged())); disconnect(pDataManager, SIGNAL(destroyed(QObject*)), this, SLOT(onDataManagerDestroyed(QObject*))); } m_pDataManager = nullptr; } bool nmWxPebiResultViewWidget::isImportedVolumeMode() const { return m_pVolumeState != nullptr && m_pVolumeState->m_bImported; } bool nmWxPebiResultViewWidget::canImportVolume() const { if(m_pVolumeState == nullptr || m_pVolumeState->m_pTask != nullptr || m_pControlBar == nullptr || m_pControlBar->isExportInProgress() || isRegionPressureDrawing()) return false; nmPebiResultPlaybackController* pPlayback = m_pControlBar->getPlaybackController(); return (pPlayback == nullptr || !pPlayback->isPlaying()) && (isImportedVolumeMode() || !m_pControlBar->isExternalInteractionLocked()); } void nmWxPebiResultViewWidget::synchronizeVolumeActions() { if(m_pVolumeState == nullptr || m_pVolumeState->m_pImportAction == nullptr) return; m_pVolumeState->m_pImportAction->setEnabled(canImportVolume()); m_pVolumeState->m_pImportAction->setText(isImportedVolumeMode() ? tr("Replace 3D Model...") : tr("Import 3D Model...")); m_pVolumeState->m_pReturnAction->setEnabled(isImportedVolumeMode() && m_pVolumeState->m_pTask == nullptr); const bool bSliceAvailable = isImportedVolumeMode() && m_pVolumeState->m_pTask == nullptr && m_pVolumeState->m_pController->hasData(); m_pVolumeState->m_pSliceAction->setEnabled(bSliceAvailable); m_pVolumeState->m_pSliceSettingsAction->setEnabled(bSliceAvailable); setActionCheckedWithoutSignal(m_pVolumeState->m_pSliceAction, isImportedVolumeMode() && m_pVolumeState->m_pController->isSliceEnabled()); if(m_pVolumeState->m_pSliceDialog != nullptr) m_pVolumeState->m_pSliceDialog->setEnabled(bSliceAvailable); synchronizeSliceInteraction(); if(!isImportedVolumeMode()) return; nmVolumeSceneController* pVolume = m_pVolumeState->m_pController; // 导入任务期间冻结底栏;平时只启用当前三维场景支持的显示及属性选择。 m_pControlBar->setExternalInteractionLocked(m_pVolumeState->m_pTask != nullptr); m_pControlBar->synchronizeFromScene(); setActionCheckedWithoutSignal(m_pScalarBarAction, pVolume->isScalarBarVisible()); m_pScalarBarAction->setEnabled(pVolume->hasScalarProperty()); // 与筛选等结果专用功能一致,保留比例网格入口,仅在外部三维模式下禁用。 m_pScaleGridAction->setEnabled(false); m_pWellNamesAction->setEnabled(false); m_pFilterAction->setEnabled(false); m_pRegionPressureAction->setEnabled(false); m_pControlBar->getExportAnimationAction()->setEnabled(false); } void nmWxPebiResultViewWidget::onImportVolumeRequested() { if(!canImportVolume()) return; const QString sFilePath = QFileDialog::getOpenFileName(this, tr("Import 3D Model"), QString(), tr("VTK Volume Mesh (*.vtk *.vtu)")); if(!sFilePath.isEmpty()) importVolumeFile(sFilePath); } bool nmWxPebiResultViewWidget::importVolumeFile(const QString& sFilePath) { if(!isResultViewGuiThread() || sFilePath.isEmpty() || !canImportVolume()) return false; m_pVolumeState->m_pTask = new nmVolumeImportTask(sFilePath, this); m_pVolumeState->m_pProgress = new QProgressDialog(tr("Loading 3D model..."), tr("Cancel"), 0, 100, this); m_pVolumeState->m_pProgress->setObjectName(QLatin1String("volumeImportProgress")); m_pVolumeState->m_pProgress->setWindowModality(Qt::WindowModal); m_pVolumeState->m_pProgress->setMinimumDuration(0); m_pVolumeState->m_pProgress->setAutoClose(false); m_pVolumeState->m_pProgress->setAutoReset(false); connect(m_pVolumeState->m_pProgress, SIGNAL(canceled()), this, SLOT(onCancelVolumeImport())); connect(m_pVolumeState->m_pTask, SIGNAL(progressChanged(int)), m_pVolumeState->m_pProgress, SLOT(setValue(int)), Qt::QueuedConnection); connect(m_pVolumeState->m_pTask, SIGNAL(finished()), this, SLOT(onVolumeImportFinished()), Qt::QueuedConnection); m_pVolumeState->m_pProgress->show(); synchronizeBusinessActions(); m_pVolumeState->m_pTask->start(); return true; } void nmWxPebiResultViewWidget::onCancelVolumeImport() { if(m_pVolumeState->m_pTask != nullptr) m_pVolumeState->m_pTask->requestCancel(); } void nmWxPebiResultViewWidget::onVolumeImportFinished() { nmVolumeImportTask* pTask = m_pVolumeState->m_pTask; if(pTask == nullptr || sender() != pTask) return; // finished 发出后仍等待线程完全退出,确保结果和字符串已不再被工作线程改写。 pTask->wait(); const bool bCanceled = pTask->isCanceled(); const QSharedPointer pData = pTask->getResult(); QString sError = pTask->getError(); disconnect(pTask, nullptr, nullptr, nullptr); delete pTask; m_pVolumeState->m_pTask = nullptr; // 模态进度更新可能嵌套处理 Qt 事件;延迟删除,避免在 setValue 栈内析构。 m_pVolumeState->m_pProgress->hide(); m_pVolumeState->m_pProgress->deleteLater(); m_pVolumeState->m_pProgress = nullptr; if(bCanceled) { synchronizeBusinessActions(); return; } nmVTKViewState oPreviousView; m_pRenderContainer->captureViewState(oPreviousView); if(pData.isNull() || !m_pVolumeState->m_pController->setData(pData, &sError)) { m_sOperationError = sError.isEmpty() ? tr("Unable to display the 3D model.") : sError; synchronizeBusinessActions(); // 空结果页也应显示导入错误,而不是把错误藏在不可见的旧结果状态中。 m_pStatusLabel->setStyleSheet("QLabel { color: #a32626; background: #fdecec; }"); m_pStatusLabel->setText(m_sOperationError); m_pStatusLabel->show(); return; } // 候选模型绑定成功后才释放旧切片交互;失败、取消路径均保留原截面及草稿。 closeVolumeSliceSettings(true); if(!isImportedVolumeMode()) { m_pVolumeState->m_oSavedView = oPreviousView; m_pVolumeState->m_eSavedCommands = m_pRenderContainer->getViewCommandController()->getCapabilities(); m_pControlBar->stopPlayback(); stopRegionPressureDrawing(); // 区域图层由 Scene 所有;先核实对象身份,避免访问已被外部清理的观察指针。 nmVTKScene* pScene = m_pRenderContainer->getScene(); const bool bHasRegionLayer = pScene != nullptr && m_pRegionState->m_pLayer != nullptr && pScene->getLayer(QString::fromLatin1(PEBI_REGION_PRESSURE_LAYER_ID)) == m_pRegionState->m_pLayer; m_pVolumeState->m_bRegionVisible = bHasRegionLayer && m_pRegionState->m_pLayer->isVisible(); m_pVolumeState->m_bRegionDialogVisible = m_pRegionState->m_pDialog != nullptr && m_pRegionState->m_pDialog->isVisible(); if(bHasRegionLayer) m_pRegionState->m_pLayer->setVisible(false); if(m_pRegionState->m_pDialog != nullptr) m_pRegionState->m_pDialog->hide(); m_pSceneController->setSuspended(true); } m_pRenderContainer->setViewCommandCapabilities(m_pVolumeState->m_eSavedCommands | nmVTKViewCommandController::ViewCommand_Right | nmVTKViewCommandController::ViewCommand_Isometric); m_pVolumeState->m_bImported = true; m_pControlBar->setVolumeSceneController(m_pVolumeState->m_pController); m_pControlBar->show(); m_pRenderContainer->restoreDefaultInteraction(); m_pVolumeState->m_pController->activate(true); m_sOperationError.clear(); synchronizeBusinessActions(); refreshStatusBar(); m_pRenderContainer->showReadyState(); } void nmWxPebiResultViewWidget::onReturnToCalculationResult() { if(!isImportedVolumeMode() || m_pVolumeState->m_pTask != nullptr) return; const QSharedPointer pLatest = m_pDataManager.isNull() ? QSharedPointer() : m_pDataManager->getPebiResultSnapshot(); const bool bSameSnapshot = !pLatest.isNull() && pLatest == m_pDisplayedSnapshot && pLatest->getSnapshotId() == m_sDisplayedSnapshotId && m_pSceneController->hasSnapshot(); if(!pLatest.isNull() && !bSameSnapshot) { QString sError; // PEBI 仍处于挂起状态,候选绑定既不显示图层,也不移动当前三维相机。 if(!m_pSceneController->setSnapshot(pLatest, &sError)) { m_sOperationError = tr("Unable to return to the latest calculation result: %1").arg(sError); refreshStatusBar(); return; } } closeVolumeSliceSettings(true); m_pVolumeState->m_pController->hide(); m_pRenderContainer->setRotationCenterPickTarget(nullptr); m_pVolumeState->m_bImported = false; m_pRenderContainer->setViewCommandCapabilities(m_pVolumeState->m_eSavedCommands); if(pLatest.isNull()) { m_pSceneController->setSuspended(false); m_pVolumeState->m_pController->clear(); clearSceneAndControls(tr("No result data.")); } else { if(!bSameSnapshot) clearRegionPressureSelection(false); m_pSceneController->setSuspended(false); if(bSameSnapshot) { m_pRenderContainer->restoreViewState(m_pVolumeState->m_oSavedView); nmVTKScene* pScene = m_pRenderContainer->getScene(); if(pScene != nullptr && m_pRegionState->m_pLayer != nullptr && pScene->getLayer(QString::fromLatin1(PEBI_REGION_PRESSURE_LAYER_ID)) == m_pRegionState->m_pLayer) { m_pRegionState->m_pLayer->setVisible(m_pVolumeState->m_bRegionVisible); } if(m_pVolumeState->m_bRegionDialogVisible && m_pRegionState->m_pDialog != nullptr) m_pRegionState->m_pDialog->show(); } else { m_pRenderContainer->fitTopViewWithoutRender(); m_pRenderContainer->resetRotationCenterWithoutRender(); } m_pVolumeState->m_pController->clear(); m_pDisplayedSnapshot = pLatest; m_sDisplayedSnapshotId = pLatest->getSnapshotId(); rememberObservedState(pLatest, m_sDisplayedSnapshotId, m_pDataManager->isPebiResultSnapshotCurrent()); m_sBindingError.clear(); m_sOperationError.clear(); m_pRenderContainer->showReadyState(); } m_pVolumeState->m_bPendingSync = false; m_pVolumeState->m_oSavedView = nmVTKViewState(); m_pControlBar->setVolumeSceneController(nullptr); m_pControlBar->setExternalInteractionLocked(false); m_pControlBar->show(); m_pControlBar->synchronizeFromScene(); synchronizeBusinessActions(); refreshStatusBar(); } void nmWxPebiResultViewWidget::closeVolumeSliceSettings(bool bReleaseData) { if(m_pVolumeState == nullptr) return; if(m_pVolumeState->m_pSliceInteraction != nullptr) m_pVolumeState->m_pSliceInteraction->unbind(); nmWxVolumeSliceSettingsDlg* pDialog = m_pVolumeState->m_pSliceDialog; if(pDialog != nullptr) { // 场景切换由外层统一 Render,不能由 hideEvent 在事务中途触发一次空白画面。 const bool bBlocked = pDialog->blockSignals(true); pDialog->hide(); if(bReleaseData) pDialog->setData(QSharedPointer(), nmVolumeSliceSettings()); pDialog->blockSignals(bBlocked); } } void nmWxPebiResultViewWidget::synchronizeSliceInteraction() { if(m_pVolumeState == nullptr || m_pVolumeState->m_pSliceInteraction == nullptr) return; nmWxVolumeSliceSettingsDlg* pDialog = m_pVolumeState->m_pSliceDialog; const bool bEnabled = isImportedVolumeMode() && m_pVolumeState->m_pTask == nullptr && m_pVolumeState->m_pController->isSliceEnabled() && pDialog != nullptr && pDialog->isVisible() && m_pVolumeState->m_pController->getSliceSettings().m_bShowPlane && !m_pRenderContainer->isRotationCenterPicking(); const bool bWasEnabled = m_pVolumeState->m_pSliceInteraction->isEnabled(); m_pVolumeState->m_pSliceInteraction->setEnabled(bEnabled); if(bWasEnabled != m_pVolumeState->m_pSliceInteraction->isEnabled()) { // 拾取信号也会在场景绑定过程中发出,不能在该调用栈内立即 Render。 QMetaObject::invokeMethod(this, "refreshVolumeSliceView", Qt::QueuedConnection); } } void nmWxPebiResultViewWidget::refreshVolumeSliceView() { if(isImportedVolumeMode()) m_pRenderContainer->renderView(); } void nmWxPebiResultViewWidget::onVolumeSliceToggled(bool bEnabled) { if(!isImportedVolumeMode() || m_pVolumeState->m_pTask != nullptr) { synchronizeVolumeActions(); return; } QString sError; if(!m_pVolumeState->m_pController->setSliceEnabled(bEnabled, &sError)) { m_sOperationError = sError.isEmpty() ? tr("Unable to display the slice.") : sError; } else { m_sOperationError.clear(); if(!bEnabled) { closeVolumeSliceSettings(false); m_pRenderContainer->renderView(); } } synchronizeVolumeActions(); refreshStatusBar(); } void nmWxPebiResultViewWidget::onShowVolumeSliceSettings() { if(!isImportedVolumeMode() || m_pVolumeState->m_pTask != nullptr) return; nmVolumeSceneController* pController = m_pVolumeState->m_pController; QString sError; if(!pController->setSliceEnabled(true, &sError)) { m_sOperationError = sError.isEmpty() ? tr("Unable to display the slice.") : sError; refreshStatusBar(); return; } if(m_pVolumeState->m_pSliceDialog == nullptr) { nmWxVolumeSliceSettingsDlg* pDialog = new nmWxVolumeSliceSettingsDlg(this); m_pVolumeState->m_pSliceDialog = pDialog; connect(pDialog, SIGNAL(applyRequested()), this, SLOT(onApplyVolumeSlice())); connect(pDialog, SIGNAL(previewChanged()), this, SLOT(onVolumeSliceDraftChanged())); connect(pDialog, SIGNAL(displayOptionsChanged(bool,bool)), this, SLOT(onVolumeSliceDisplayChanged(bool,bool))); connect(pDialog, SIGNAL(dialogHidden()), this, SLOT(onVolumeSliceDialogHidden())); } nmWxVolumeSliceSettingsDlg* pDialog = m_pVolumeState->m_pSliceDialog; if(!pDialog->isVisible()) { const QSharedPointer pData = pController->getData(); if(pData.isNull()) return; const nmVolumeSliceSettings oSettings = pController->getSliceSettings(); pDialog->setData(pData, oSettings); double dBounds[6]; pData->getBounds(dBounds); m_pVolumeState->m_pSliceInteraction->bind(pData->getDataId(), dBounds, oSettings); } pDialog->show(); pDialog->raise(); pDialog->activateWindow(); m_sOperationError.clear(); synchronizeVolumeActions(); refreshStatusBar(); m_pRenderContainer->renderView(); } void nmWxPebiResultViewWidget::onApplyVolumeSlice() { if(!isImportedVolumeMode() || m_pVolumeState->m_pTask != nullptr || m_pVolumeState->m_pSliceDialog == nullptr || !m_pVolumeState->m_pSliceDialog->isVisible()) return; nmVolumeSliceSettings oSettings; QString sError; if(!m_pVolumeState->m_pSliceDialog->getDraft(oSettings, &sError)) { m_pVolumeState->m_pSliceDialog->setError(sError); return; } if(!m_pVolumeState->m_pController->applySliceSettings(oSettings, &sError)) { m_sOperationError = sError.isEmpty() ? tr("Unable to display the slice.") : sError; m_pVolumeState->m_pSliceDialog->setError(m_sOperationError); } else { m_sOperationError.clear(); const nmVolumeSliceSettings oApplied = m_pVolumeState->m_pController->getSliceSettings(); m_pVolumeState->m_pSliceDialog->setAppliedSettings(oApplied); m_pVolumeState->m_pSliceInteraction->setPreview(oApplied); } synchronizeVolumeActions(); refreshStatusBar(); } void nmWxPebiResultViewWidget::onVolumeSliceDraftChanged() { if(!isImportedVolumeMode() || m_pVolumeState->m_pTask != nullptr || m_pVolumeState->m_pSliceDialog == nullptr) return; nmVolumeSliceSettings oDraft; if(m_pVolumeState->m_pSliceDialog->getDraft(oDraft)) { m_pVolumeState->m_pSliceInteraction->setPreview(oDraft); m_pRenderContainer->renderView(); } } void nmWxPebiResultViewWidget::onVolumeSliceDisplayChanged(bool bShowPlane, bool bShowSource) { if(!isImportedVolumeMode() || m_pVolumeState->m_pTask != nullptr) return; m_pVolumeState->m_pController->setSliceDisplayOptions(bShowPlane, bShowSource); synchronizeSliceInteraction(); m_pRenderContainer->renderView(); } void nmWxPebiResultViewWidget::onVolumeSliceDialogHidden() { if(m_pVolumeState == nullptr || m_pVolumeState->m_pSliceInteraction == nullptr) return; m_pVolumeState->m_pSliceInteraction->unbind(); m_pRenderContainer->renderView(); } void nmWxPebiResultViewWidget::onVolumeSlicePreview(const QString& sDataId, const nmVolumeSliceSettings& oSettings) { if(!isImportedVolumeMode() || m_pVolumeState->m_pTask != nullptr || !m_pVolumeState->m_pController->isSliceEnabled() || m_pVolumeState->m_pSliceDialog == nullptr || !m_pVolumeState->m_pSliceDialog->isVisible()) return; const QSharedPointer pData = m_pVolumeState->m_pController->getData(); if(pData.isNull() || pData->getDataId() != sDataId) return; m_pVolumeState->m_pSliceDialog->setPlanePreview(oSettings); } void nmWxPebiResultViewWidget::onVolumeSliceDragApplied(const QString& sDataId, const nmVolumeSliceSettings& oSettings) { Q_UNUSED(oSettings); if(!isImportedVolumeMode() || m_pVolumeState->m_pTask != nullptr || !m_pVolumeState->m_pController->isSliceEnabled()) return; const QSharedPointer pData = m_pVolumeState->m_pController->getData(); if(pData.isNull() || pData->getDataId() != sDataId) return; onApplyVolumeSlice(); }