You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
nmWTAI-Platform/Src/nmNum/nmSubWxs/nmWxPebiResultViewWidget.cpp

2839 lines
113 KiB
C++

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#include "nmWxPebiResultViewWidget.h"
#include "nmVolumeImportTask.h"
#include "nmVolumeSceneController.h"
#include "nmVolumeSliceInteraction.h"
#include "nmWxVolumeSliceSettingsDlg.h"
#include "nmVTKViewState.h"
#include <QFileDialog>
#include <QToolButton>
#include <QPainter>
#include <QPixmap>
#include <QPolygon>
#include <QPen>
#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 <QAction>
#include <QAtomicInt>
#include <QCheckBox>
#include <QComboBox>
#include <QCoreApplication>
#include <QDialog>
#include <QDir>
#include <QGridLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QIcon>
#include <QLabel>
#include <QLineEdit>
#include <QLocale>
#include <QMenu>
#include <QMessageBox>
#include <QMetaObject>
#include <QPointer>
#include <QProgressDialog>
#include <QPushButton>
#include <QSize>
#include <QSizePolicy>
#include <QThread>
#include <QToolBar>
#include <QVBoxLayout>
#include <vtkCamera.h>
#include <vtkCallbackCommand.h>
#include <vtkMath.h>
#include <vtkObject.h>
#include <vtkRenderer.h>
#include <vtkSmartPointer.h>
#include <math.h>
#if !defined(_MSC_VER)
#include <cmath>
#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<nmWxPebiResultControlBar> 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<QPointF> m_vecPendingPoints; ///< 完成回调复制出的候选世界坐标。
nmPebiRegionPressureCalculator m_oCalculator; ///< 独占相交缓存和压力计算器。
nmVTKPlanarPolygonLayer* m_pLayer; ///< 业务 Scene 所有的区域轮廓观察指针。
vtkSmartPointer<nmVTKPlanarPolygonInteractorStyle> m_pStyle; ///< 临时绘制样式。
vtkSmartPointer<vtkCallbackCommand> m_pCallback; ///< 不拥有窗口的 VTK 回调命令。
unsigned long m_nChangedObserverTag; ///< 点集变化观察者标识。
unsigned long m_nCompletedObserverTag; ///< 绘制完成观察者标识。
unsigned long m_nCancelledObserverTag; ///< 绘制取消观察者标识。
unsigned long m_nInvalidObserverTag; ///< 无效完成观察者标识。
QSharedPointer<const nmPebiResultSnapshot> 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<double, double> 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<double, double> 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<const nmPebiResultSnapshot>(),
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<nmWxPebiResultViewWidget*>(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<QPointF> 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<const nmPebiResultSnapshot> 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<double> 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<vtkCamera>::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<const nmPebiResultSnapshot>(),
QString(), false);
return;
}
const QSharedPointer<const nmPebiResultSnapshot> 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<nmVTKPlanarPolygonInteractorStyle>::New();
m_pRegionState->m_pCallback =
vtkSmartPointer<vtkCallbackCommand>::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<qlonglong>(
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<qlonglong>(
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<const nmPebiResultSnapshot>& 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<const nmVolumeData> 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<const nmPebiResultSnapshot> pLatest = m_pDataManager.isNull()
? QSharedPointer<const nmPebiResultSnapshot>() : 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<const nmVolumeData>(), 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<const nmVolumeData> 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<const nmVolumeData> 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<const nmVolumeData> pData = m_pVolumeState->m_pController->getData();
if(pData.isNull() || pData->getDataId() != sDataId) return;
onApplyVolumeSlice();
}