feat(numerical): 完成 PEBI 结果界面快照化

- 结果井、曲线、三维场图及参数页统一读取结果快照
- 支持已删除井继续显示历史结果
- 排除无产量井的结果下拉项并修正空曲线校验
- 使用受控 VTK 适配器降低动画和导出内存开销
feature/pebi-result-snapshot-20260831
lh 2 weeks ago
parent acb39fe350
commit 3bf169cd54

@ -241,6 +241,13 @@ public:
*/
nmDataWellBase* findWellByCode(const QString& sWellCode) const;
/**
* @brief UUID
* @note UUID
*/
nmDataWellBase* findWellByInstanceId(
const QString& sWellInstanceId) const;
// 初始化测试井数据,即点击数值解的时候,将当前井添加到里面
void initCurWellData();

@ -160,8 +160,8 @@ class NM_SUB_WND_EXPORT nmSubWndMain : public iSubWndBaseFit {
void onPebiGridGenerationFinished(quint64 nGridInputRevision,
bool bSucceeded);
/** @brief 按 WellCode 切换结果曲线,不改变主分析井和参数编辑井。 */
void onWellSelected(const QString& sWellCode);
/** @brief 按快照井 UUID 切换结果曲线,不改变实时井或参数编辑井。 */
void onWellSelected(const QString& sWellInstanceId);
// 点击左侧按钮求解生成操作
void onGenerateButtonClicked();
@ -239,6 +239,11 @@ class NM_SUB_WND_EXPORT nmSubWndMain : public iSubWndBaseFit {
// 填充已经存在的数值三维和结果参数页面。
bool populateResultWidgets(iSubWndFitting* pSubWndFit,
nmDataAnalyzeManager* pDataManager);
/** @brief 用快照中冻结的历史和计算曲线刷新三个结果窗口。 */
bool refreshPebiResultCurves(
iSubWndFitting* pSubWndFit,
nmDataAnalyzeManager* pDataManager,
const QString& sWellInstanceId);
// 将首次结果载体的页面填充排队到当前创建事件结束之后。
void schedulePendingResultWidgets();
// 根据结果页面是否准备成功显示本次求解的最终提示。

@ -63,15 +63,15 @@ public:
/** @brief 仅刷新属于指定数据管理器的活动面板结果井列表。 */
static void notifyResultWellSelectorChanged(
nmDataAnalyzeManager* pDataManager,
const QString& sWellCode = QString());
const QString& sWellInstanceId = QString());
/** @brief 将本面板设为活动实例,并按所属数据管理器刷新结果井列表。 */
void activateCurrentInstance();
signals:
void sigGenerateClicked(); // 点击生成按钮时发出的信号
void sigIncludeWells(); // 包含其他井后刷新参数栏的信号
/** @brief 用户切换结果井时发送稳定 WellCode。 */
void sigResultWellChanged(const QString& sWellCode);
/** @brief 用户切换结果井时发送快照中的稳定井 UUID。 */
void sigResultWellChanged(const QString& sWellInstanceId);
private slots:
// 新增的槽函数
@ -144,8 +144,9 @@ private:
QString& sError);
// 将焦点定位到第一个非法时间步字段
void focusFirstInvalidTimeStepField();
/** @brief 按统一结果资格填充下拉框,条目数据保存 WellCode。 */
void fillResultWellCombo(const QString& sSelectedWellCode = QString());
/** @brief 按快照显示顺序填充结果井,条目数据保存求解时井 UUID。 */
void fillResultWellCombo(
const QString& sSelectedWellInstanceId = QString());
// 刷新当前模型显示
void updateCurrentModelDisplay();
// 刷新当前井的时间变表皮状态
@ -168,7 +169,7 @@ private:
// 模型与结果切换控件
QGroupBox* m_pModelAndResultGroup; ///< 当前模型与结果井控件分组。
QComboBox* m_pResultWellCombo; ///< 可展示结果的井列表itemData 保存 WellCode
QComboBox* m_pResultWellCombo; ///< 快照结果井列表itemData 保存求解时井 UUID
bool m_bFillingResultWellCombo; ///< 程序填充结果井时屏蔽切换槽。
QComboBox* m_pCurrentModelCombo; ///< 当前单相数值模型显示与选择控件。
bool m_bFillingCurrentModelCombo; ///< 程序刷新模型时屏蔽切换槽。

@ -7,6 +7,7 @@
#include <QList>
#include <QMap>
#include <QPointer>
#include <QSharedPointer>
class QVBoxLayout;
class QHBoxLayout;
@ -23,6 +24,8 @@ class QImage;
class QPrinter;
class nmDataAnalyzeManager;
class nmPebiResultSnapshot;
class nmPebiResultSnapshotRenderAdapter;
#include <vtkSmartPointer.h> // 必须包含这个头文件来使用 vtkSmartPointer
@ -141,15 +144,15 @@ private:
void initVTKWidget();
void initOperPannel();
// 初始化井的绘制相关 VTK 对象(井名和井线)
/** @brief 按快照中的求解时名称和位置初始化井标注及井线。 */
void initWellDrawing();
// 清除所有井相关的绘制 Actor
/** @brief 清除所有井相关 Actor释放旧快照显示状态。 */
void clearWellActors();
// 初始化时间步列表从nmDataAnalyzeManager获取
/** @brief 按快照压力帧顺序初始化时间步列表。 */
void initTimeSteps();
// 渲染指定索引时间步的数据
/** @brief 通过受控适配器绑定并渲染指定压力帧。 */
void loadDataForIndex(int index);
// 将VTK渲染内容转换为QImage
@ -200,6 +203,10 @@ private slots:
private:
/** @brief 构造时绑定的分析数据管理器;销毁后 QPointer 自动置空。 */
QPointer<nmDataAnalyzeManager> m_pDataManager;
/** @brief 当前窗口持有的最后成功结果只读快照。 */
QSharedPointer<const nmPebiResultSnapshot> m_pResultSnapshot;
/** @brief 独占渲染网格和压力帧绑定状态的受控适配器。 */
nmPebiResultSnapshotRenderAdapter* m_pResultRenderAdapter;
// 布局
QVBoxLayout* m_pMainLayout;
@ -232,12 +239,9 @@ private:
vtkSmartPointer<vtkLookupTable> m_lookupTable; // VTK 颜色查找表 (定义颜色映射规则)
vtkSmartPointer<vtkTextActor> m_textActor; // VTK 文本演员 (用于显示时间步信息)
// 渲染的基础网格
vtkSmartPointer<vtkUnstructuredGrid> m_pCachedBaseGrid;
// === 新增:用于绘制井的 VTK 对象 (全部使用 vtkActor 指针,因为 vtkTextActor3D/vtkBillboardTextActor3D 都继承自 vtkActor) ===
QMap<QString, vtkSmartPointer<vtkBillboardTextActor3D>> m_mapWellNameActors; ///< WellCode 到井名文本 Actor 的映射。
QMap<QString, vtkSmartPointer<vtkActor>> m_mapWellLineActors; ///< WellCode 到井线 Actor 的映射。
QMap<QString, vtkSmartPointer<vtkBillboardTextActor3D>> m_mapWellNameActors; ///< 快照井 UUID 到井名 Actor 的映射。
QMap<QString, vtkSmartPointer<vtkActor>> m_mapWellLineActors; ///< 快照井 UUID 到井线 Actor 的映射。
bool m_bShowWellActors; // 是否显示井名和井线
// 阈值过滤器

@ -210,7 +210,10 @@ bool captureResultWellMetadata(
pWellData->getRadius().getValue().toDouble();
oWellInput.m_bHasPerforation =
pWellData->getPerforationCount() > 0;
oWellInput.m_bDisplayResultWell = bDisplayResultWell;
// 无产量井仍作为观察井保留在求解槽位和快照元数据中,但不进入结果下拉框。
oWellInput.m_bDisplayResultWell = bDisplayResultWell &&
oWellInput.m_bRateControlled &&
oWellInput.m_vecFlowPoints.size() >= 2;
nmDataVerticalFracturedWell* pVerticalFracturedWell =
dynamic_cast<nmDataVerticalFracturedWell*>(pWellData);

@ -1107,6 +1107,23 @@ nmDataWellBase* nmDataAnalyzeManager::findWellByCode(
return nullptr;
}
nmDataWellBase* nmDataAnalyzeManager::findWellByInstanceId(
const QString& sWellInstanceId) const
{
if(sWellInstanceId.isEmpty()) {
return nullptr;
}
foreach(nmDataWellBase* pWell, m_vWellData) {
if(pWell != nullptr &&
pWell->getWellInstanceId() == sWellInstanceId) {
return pWell;
}
}
return nullptr;
}
void nmDataAnalyzeManager::initCurWellData()
{
// 获取当前默认井数据

@ -81,7 +81,7 @@ bool isCurveValid(const QVector<QVector<double> >& vecCurve,
{
return !bRequired;
}
if(vecCurve.size() < nMinimumSeries || vecCurve[0].isEmpty())
if(vecCurve.size() < nMinimumSeries)
{
return false;
}
@ -95,7 +95,9 @@ bool isCurveValid(const QVector<QVector<double> >& vecCurve,
return false;
}
}
return true;
// 观察井没有产量制度时,双对数和半对数按固定列数保存空列。
// 必填曲线仍必须包含数据,选填曲线允许“列结构存在但点数为零”。
return nPointCount > 0 || !bRequired;
}
bool areWellCurvesValid(const nmPebiResultWellCurves& oCurves)
@ -727,10 +729,15 @@ bool nmPebiResultSnapshotBuilder::validate(QString* pError)
(oWell.m_bHasSkin && !isFiniteValue(oWell.m_dSkin)) ||
(oWell.m_bHasDfc && !isFiniteValue(oWell.m_dDfc)) ||
setWellIds.contains(oWell.m_sWellInstanceId) ||
setWellCodes.contains(oWell.m_sWellCode) ||
!areWellCurvesValid(oWell.m_oCurves))
setWellCodes.contains(oWell.m_sWellCode))
{
return fail(QString("PEBI result well metadata are invalid: %1")
.arg(oWell.m_sWellCode), pError);
}
if(!areWellCurvesValid(oWell.m_oCurves))
{
return fail("PEBI result well data are invalid.", pError);
return fail(QString("PEBI result well curves are invalid: %1")
.arg(oWell.m_sWellCode), pError);
}
setWellIds.insert(oWell.m_sWellInstanceId);
setWellCodes.insert(oWell.m_sWellCode);

@ -48,6 +48,7 @@
#include "nmObjPointWell.h"
#include "nmDataAnalyzeManager.h"
#include "nmPebiResultSnapshot.h"
#include "nmDataDemo.h"
#include "nmDataLogFile.h"
#include "nmDataMeasure.h"
@ -1955,13 +1956,6 @@ bool nmSubWndMain::mergeAnaResultToFitting()
return false;
}
// 历史压力数据
QVector<QVector<double >> vvecHistoryData;
// 双对数数据
QVector<QVector<double >> vvecLogPreData;
// 半对数数据
QVector<QVector<double >> vvecSemiLogPreData;
nmDataAnalyzeManager* pInstance = !m_pSolverDataManager.isNull()
? m_pSolverDataManager.data()
: nmDataAnalyzeManager::getCurrentInstance();
@ -1983,80 +1977,35 @@ bool nmSubWndMain::mergeAnaResultToFitting()
m_nPendingResultPebiCount = -1;
m_nPendingResultSolveTimeMs = -1;
QVector<nmSolverWellRef> vecWellsOrder =
pInstance->hasPebiResults()
? pInstance->getResultSolverWellOrder()
: pInstance->getSolverWellOrder();
QString sCurrentResultWellCode = pInstance->getCurrentResultWellCode();
// 当前结果井必须同时位于最后一次结果顺序并具备结果展示资格。
// 无流量观察井虽然有内部压力结果,但不能成为曲线窗口的当前结果井。
const bool bResultWellAvailable =
pInstance->isWellAvailableForResult(sCurrentResultWellCode);
bool bResultWellFound = false;
for(int nIndex = 0; nIndex < vecWellsOrder.size(); ++nIndex) {
const nmSolverWellRef& oWellRef = vecWellsOrder[nIndex];
if(oWellRef.m_eEntryKind != NM_SolverEntry_Well) {
continue;
}
if(bResultWellAvailable &&
oWellRef.m_sWellCode == sCurrentResultWellCode) {
bResultWellFound = true;
break;
if(pInstance->getGridType() == NM_Grid_PEBI) {
QSharedPointer<const nmPebiResultSnapshot> pSnapshot =
pInstance->getPebiResultSnapshot();
if(pSnapshot.isNull()) {
return false;
}
}
if(!bResultWellFound) {
for(int nIndex = 0; nIndex < vecWellsOrder.size(); ++nIndex) {
if(vecWellsOrder[nIndex].m_eEntryKind == NM_SolverEntry_Well &&
pInstance->isWellAvailableForResult(
vecWellsOrder[nIndex].m_sWellCode)) {
sCurrentResultWellCode = vecWellsOrder[nIndex].m_sWellCode;
break;
}
QString sWellInstanceId =
pInstance->getCurrentResultWellInstanceId();
if(!pSnapshot->isDisplayWell(sWellInstanceId)) {
pInstance->setCurrentResultWellInstanceId(QString());
sWellInstanceId =
pInstance->getCurrentResultWellInstanceId();
}
}
nmDataWellBase* pWellData = NULL;
if(pInstance->getGridType() == NM_Grid_PEBI) {
pWellData = pInstance->findWellByCode(sCurrentResultWellCode);
if(pWellData == NULL) {
// 曲线及下拉框都使用同一 UUID显示列表为空时曲线入口收到空值并被清空。
if(!refreshPebiResultCurves(pSubWndFit,
pInstance,
sWellInstanceId)) {
return false;
}
pInstance->setCurrentResultWellCode(sCurrentResultWellCode);
// 获取该井下存储的三条数据
vvecHistoryData = pWellData->getResultPressure();
vvecLogPreData = pWellData->getResultLogLog();
vvecSemiLogPreData = pWellData->getResultSemiLog();
}
// 计算完成后同步查看井下拉框选中项
nmWxNumericalDesign::notifyResultWellSelectorChanged(
pInstance,
sCurrentResultWellCode);
// 半对数
QVector<iCurveDesc> vecHalfLog;
this->inithalfLog(vvecSemiLogPreData, vecHalfLog, false);
FitSubRstTag tag = FSRT_SemiLog; // 半对数
QString errorMessage;
pSubWndFit->adjustFitSubPlotBy(tag, vecHalfLog, true, &errorMessage);
// 双对数
QVector<iCurveDesc> vecLogLog;
this->initloglog(vvecLogPreData, vecLogLog, false);
tag = FSRT_DoubleLog;
pSubWndFit->adjustFitSubPlotBy(tag, vecLogLog, true, &errorMessage);
// 历史压力曲线
QVector<iCurveDesc> vecHistory;
this->initPreHistory(vvecHistoryData, vecHistory, false);
tag = FSRT_Hist;
QString errorMessage4;
pSubWndFit->adjustFitSubPlotBy(tag, vecHistory, true, &errorMessage4);
nmWxNumericalDesign::notifyResultWellSelectorChanged(
pInstance, sWellInstanceId);
}
if(pInstance->getGridType() != NM_Grid_PEBI) {
return true;
}
QString errorMessage;
// getFitSubRstWxOf(..., true) 会同步创建 iWxResult、挂入 MDI 并切换当前页。
// 首次创建时只建立外层载体,待本轮 show/resize/layout 事件结束后再填充内容。
QWidget* p3DWidget = pSubWndFit->getFitSubRstWxOf(
@ -2710,94 +2659,100 @@ nmGuiPlot * nmSubWndMain::getWxPlot() const
return m_pWxPlot;
}
void nmSubWndMain::onWellSelected(const QString& sWellCode)
bool nmSubWndMain::refreshPebiResultCurves(
iSubWndFitting* pSubWndFit,
nmDataAnalyzeManager* pDataManager,
const QString& sWellInstanceId)
{
nmDataAnalyzeManager* pInstance = nmDataAnalyzeManager::getCurrentInstance();
if(pInstance == nullptr) {
return;
if(pSubWndFit == nullptr || pDataManager == nullptr ||
pDataManager->getGridType() != NM_Grid_PEBI) {
return false;
}
nmDataWellBase* pWellData = pInstance->findWellByCode(sWellCode);
if(pWellData == NULL)
return;
// 切换结果井只影响曲线展示,不再改变参数编辑井或主分析井。
pInstance->setCurrentResultWellCode(sWellCode);
// 1.获取流动段分析窗口
iSubWndFitting* pSubWndFit = nmDataAnalyzeManager::getCurrentFitting();
Q_ASSERT(pSubWndFit != nullptr);
if(!pSubWndFit) return;
// 2.根据选择的井名获取计算数据
QVector<QVector<double>> vvecHistoryData;
QVector<QVector<double>> vvecLogPreData;
QVector<QVector<double>> vvecSemiLogPreData;
QSharedPointer<const nmPebiResultSnapshot> pSnapshot =
pDataManager->getPebiResultSnapshot();
if(pSnapshot.isNull() ||
(!sWellInstanceId.isEmpty() &&
!pSnapshot->isDisplayWell(sWellInstanceId))) {
return false;
}
if(pInstance->getGridType() == NM_Grid_PEBI) {
vvecHistoryData = pWellData->getResultPressure();
vvecLogPreData = pWellData->getResultLogLog();
vvecSemiLogPreData = pWellData->getResultSemiLog();
}
// 调用函数
QVector<QVector<double>> vvecHistoryPressureData; //压力历史数据
QVector<QVector<double>> vvecHistoryLogData; // 历史双对数曲线数据
QVector<QVector<double>> vvecHistorySemiLogData; // 历史半对数曲线数据
pInstance->calculationLogData(pWellData, vvecHistoryPressureData, vvecHistoryLogData, vvecHistorySemiLogData);
// 删除末尾无效元素
//if (!vvecHistoryLogData.empty() &&
// vvecHistoryLogData.size() >= 3 && // 确保有3个子数组
// !vvecHistoryLogData[0].empty() && // 检查x数组非空
// !vvecHistoryLogData[1].empty() && // 检查y数组非空
// !vvecHistoryLogData[2].empty()) { // 检查z数组非空
// // 同时删除x、y、z的最后一个数据点
// vvecHistoryLogData[0].removeLast(); // 删除最后一个x
// vvecHistoryLogData[1].removeLast(); // 删除最后一个y
// vvecHistoryLogData[2].removeLast(); // 删除最后一个z
//}
const nmPebiResultWellSnapshot* pWell = sWellInstanceId.isEmpty()
? NULL : pSnapshot->findWell(sWellInstanceId);
if(!sWellInstanceId.isEmpty() && pWell == NULL) {
return false;
}
if(!pDataManager->setCurrentResultWellInstanceId(sWellInstanceId)) {
return false;
}
// 窗口标识,半对数、双对数、历史压力
FitSubRstTag tag;
// 错误信息
QString errorMessage;
nmPebiResultWellCurves oEmptyCurves;
const nmPebiResultWellCurves& oCurves = pWell != NULL
? pWell->m_oCurves : oEmptyCurves;
QString sErrorMessage;
// 3、半对数曲线
// 旧绘图辅助函数会裁掉双对数末点,必须使用隐式共享局部副本,
// 触发写时复制后也不能改动发布后的快照容器。
QVector<QVector<double> > vecHistorySemiLog =
oCurves.m_vecHistorySemiLog;
QVector<QVector<double> > vecResultSemiLog =
oCurves.m_vecResultSemiLog;
QVector<QVector<double> > vecHistoryLogLog =
oCurves.m_vecHistoryLogLog;
QVector<QVector<double> > vecResultLogLog =
oCurves.m_vecResultLogLog;
QVector<QVector<double> > vecHistoryPressure =
oCurves.m_vecHistoryPressure;
QVector<QVector<double> > vecResultPressureData =
oCurves.m_vecResultPressure;
// 每个窗口先发布求解时冻结的历史曲线,再叠加同一快照中的计算曲线。
QVector<iCurveDesc> vecHistoryHalfLog;
this->inithalfLog(vvecHistorySemiLogData, vecHistoryHalfLog, true);
tag = FSRT_SemiLog;
// 删除原来的线,并将当前查看井的历史半对数曲线渲染出来
pSubWndFit->adjustFitSubPlotBy(tag, vecHistoryHalfLog, true, &errorMessage);
//计算出来的半对数数据,同样需要删除原来的
this->inithalfLog(vecHistorySemiLog,
vecHistoryHalfLog, true);
pSubWndFit->adjustFitSubPlotBy(
FSRT_SemiLog, vecHistoryHalfLog, true, &sErrorMessage);
QVector<iCurveDesc> vecHalfLog;
this->inithalfLog(vvecSemiLogPreData, vecHalfLog, false);
pSubWndFit->adjustFitSubPlotBy(tag, vecHalfLog, true, &errorMessage);
this->inithalfLog(vecResultSemiLog, vecHalfLog, false);
pSubWndFit->adjustFitSubPlotBy(
FSRT_SemiLog, vecHalfLog, true, &sErrorMessage);
// 双对数
QVector<iCurveDesc> vecHistoryLog;
this->initloglog(vvecHistoryLogData, vecHistoryLog, true);
tag = FSRT_DoubleLog;
// 删除原来的线,并将当前查看井的历史双对数曲线渲染出来
pSubWndFit->adjustFitSubPlotBy(tag, vecHistoryLog, true, &errorMessage);
//计算出来的双对数数据,同样需要删除原来的
this->initloglog(vecHistoryLogLog,
vecHistoryLog, true);
pSubWndFit->adjustFitSubPlotBy(
FSRT_DoubleLog, vecHistoryLog, true, &sErrorMessage);
QVector<iCurveDesc> vecLogLog;
this->initloglog(vvecLogPreData, vecLogLog, false);
pSubWndFit->adjustFitSubPlotBy(tag, vecLogLog, true, &errorMessage);
this->initloglog(vecResultLogLog, vecLogLog, false);
pSubWndFit->adjustFitSubPlotBy(
FSRT_DoubleLog, vecLogLog, true, &sErrorMessage);
// 历史压力曲线
QVector<iCurveDesc> vecHistoryPre;
this->initPreHistory(vvecHistoryPressureData, vecHistoryPre, true);
tag = FSRT_Hist;
// 删除原来的线,并将当前查看井的历史压力曲线渲染出来
pSubWndFit->adjustFitSubPlotBy(tag, vecHistoryPre, true, &errorMessage);
//计算出来的压力数据,同样需要删除原来的
QVector<iCurveDesc> vecHistory;
this->initPreHistory(vvecHistoryData, vecHistory, false);
pSubWndFit->adjustFitSubPlotBy(tag, vecHistory, true, &errorMessage);
this->initPreHistory(vecHistoryPressure,
vecHistoryPre, true);
pSubWndFit->adjustFitSubPlotBy(
FSRT_Hist, vecHistoryPre, true, &sErrorMessage);
QVector<iCurveDesc> vecResultPressure;
this->initPreHistory(vecResultPressureData,
vecResultPressure, false);
pSubWndFit->adjustFitSubPlotBy(
FSRT_Hist, vecResultPressure, true, &sErrorMessage);
return true;
}
void nmSubWndMain::onWellSelected(const QString& sWellInstanceId)
{
nmDataAnalyzeManager* pInstance =
nmDataAnalyzeManager::getCurrentInstance();
iSubWndFitting* pSubWndFit =
nmDataAnalyzeManager::getCurrentFitting();
if(pInstance == nullptr || pSubWndFit == nullptr) {
return;
}
// 删除的实时井仍可按快照 UUID 切换;本路径不得查询或重算实时井数据。
refreshPebiResultCurves(
pSubWndFit, pInstance, sWellInstanceId);
}
#include <Windows.h>

@ -28,6 +28,7 @@
#include "nmDataWellBase.h"
#include "nmDataReservoir.h"
#include "nmDataAnalyzeManager.h"
#include "nmPebiResultSnapshot.h"
#include "nmDataTimeStepSetting.h"
#include "nmDataDiagnostic.h"
@ -250,7 +251,7 @@ void nmWxNumericalDesign::activateCurrentInstance()
// 外部刷新当前查看井下拉框
void nmWxNumericalDesign::notifyResultWellSelectorChanged(
nmDataAnalyzeManager* pDataManager,
const QString& sWellCode)
const QString& sWellInstanceId)
{
// 静态活动面板可能已经切换到另一个成果,只刷新所属数据管理器一致的实例。
if(s_pCurrentInstance == nullptr ||
@ -258,7 +259,7 @@ void nmWxNumericalDesign::notifyResultWellSelectorChanged(
return;
}
s_pCurrentInstance->fillResultWellCombo(sWellCode);
s_pCurrentInstance->fillResultWellCombo(sWellInstanceId);
}
// 初始化模型与结果切换控件
@ -354,8 +355,9 @@ void nmWxNumericalDesign::updateCurrentModelDisplay()
m_bFillingCurrentModelCombo = false;
}
// 按参与计算井刷新下拉框
void nmWxNumericalDesign::fillResultWellCombo(const QString& sSelectedWellCode)
// 按最后成功快照刷新结果井下拉框
void nmWxNumericalDesign::fillResultWellCombo(
const QString& sSelectedWellInstanceId)
{
if(m_pResultWellCombo == nullptr) {
return;
@ -371,60 +373,46 @@ void nmWxNumericalDesign::fillResultWellCombo(const QString& sSelectedWellCode)
return;
}
QVector<QString> vecResultWellCodes;
QVector<nmSolverWellRef> vecSolverOrder =
pManager->hasPebiResults()
? pManager->getResultSolverWellOrder()
: pManager->getSolverWellOrder();
// 第一步:有结果时优先按最后结果顺序,否则按当前网格顺序收集结果井。
// 自动加入的无流量观察井仍在求解器顺序中,但不能出现在本下拉框。
for(int nOrderIndex = 0; nOrderIndex < vecSolverOrder.size(); ++nOrderIndex) {
const nmSolverWellRef& oWellRef = vecSolverOrder[nOrderIndex];
if(oWellRef.m_eEntryKind == NM_SolverEntry_Well &&
pManager->isWellAvailableForResult(oWellRef.m_sWellCode) &&
!vecResultWellCodes.contains(oWellRef.m_sWellCode)) {
vecResultWellCodes.append(oWellRef.m_sWellCode);
}
QSharedPointer<const nmPebiResultSnapshot> pSnapshot =
pManager->getPebiResultSnapshot();
if(pSnapshot.isNull()) {
m_pResultWellCombo->setEnabled(false);
m_bFillingResultWellCombo = false;
return;
}
// 第二步:尚未生成网格时从当前有效计算井中执行同一结果资格过滤
if(vecResultWellCodes.isEmpty()) {
QVector<nmCalculationWellRef> vecEffectiveWells =
pManager->getEffectiveCalculationWells();
for(int nWellIndex = 0; nWellIndex < vecEffectiveWells.size(); ++nWellIndex) {
const QString sWellCode = vecEffectiveWells[nWellIndex].m_sWellCode;
if(pManager->isWellAvailableForResult(sWellCode)) {
vecResultWellCodes.append(sWellCode);
}
// 名称和顺序始终来自求解时快照;实时 UUID 不存在时只增加删除标记。
const QStringList& listWellIds =
pSnapshot->getDisplayWellInstanceIds();
for(int nWellIndex = 0; nWellIndex < listWellIds.size(); ++nWellIndex) {
const QString& sWellInstanceId = listWellIds[nWellIndex];
const nmPebiResultWellSnapshot* pWell =
pSnapshot->findWell(sWellInstanceId);
if(pWell == nullptr) {
continue;
}
}
for(int nWellIndex = 0; nWellIndex < vecResultWellCodes.size(); ++nWellIndex) {
nmDataWellBase* pWellData =
pManager->findWellByCode(vecResultWellCodes[nWellIndex]);
if(pWellData != nullptr) {
m_pResultWellCombo->addItem(pWellData->getWellName(),
pWellData->getWellCode());
QString sDisplayName = pWell->m_sWellName;
if(pManager->findWellByInstanceId(sWellInstanceId) == nullptr) {
sDisplayName = tr("%1 (Deleted)").arg(sDisplayName);
}
m_pResultWellCombo->addItem(sDisplayName, sWellInstanceId);
}
QString sCurrentWellCode = sSelectedWellCode;
if(sCurrentWellCode.isEmpty()) {
sCurrentWellCode = pManager->getCurrentResultWellCode();
}
if(sCurrentWellCode.isEmpty()) {
sCurrentWellCode = pManager->getPrimaryWellCode();
QString sCurrentWellInstanceId = sSelectedWellInstanceId;
if(sCurrentWellInstanceId.isEmpty()) {
sCurrentWellInstanceId =
pManager->getCurrentResultWellInstanceId();
}
int nIndex = m_pResultWellCombo->findData(sCurrentWellCode);
int nIndex = m_pResultWellCombo->findData(sCurrentWellInstanceId);
if(nIndex < 0 && m_pResultWellCombo->count() > 0) {
nIndex = 0;
}
if(nIndex >= 0) {
m_pResultWellCombo->setCurrentIndex(nIndex);
pManager->setCurrentResultWellCode(
pManager->setCurrentResultWellInstanceId(
m_pResultWellCombo->itemData(nIndex).toString());
}
@ -938,19 +926,20 @@ void nmWxNumericalDesign::onResultWellChanged(int index)
return;
}
QString sWellCode = m_pResultWellCombo->itemData(index).toString();
if(sWellCode.isEmpty()) {
const QString sWellInstanceId =
m_pResultWellCombo->itemData(index).toString();
if(sWellInstanceId.isEmpty()) {
return;
}
nmDataAnalyzeManager* pManager = m_pDataManager;
if(pManager != nullptr) {
// 结果井切换只更新查看状态,不改变主分析井和参数编辑井。
pManager->setCurrentResultWellCode(sWellCode);
pManager->setCurrentResultWellInstanceId(sWellInstanceId);
}
// 复用主窗口已有的井结果刷新逻辑
emit sigResultWellChanged(sWellCode);
emit sigResultWellChanged(sWellInstanceId);
}
void nmWxNumericalDesign::onIncludeOtherWellsToggled(bool bChecked)
@ -961,13 +950,15 @@ void nmWxNumericalDesign::onIncludeOtherWellsToggled(bool bChecked)
// 总开关只控制有效计算井集合;已经勾选的井保留,重新打开时无需再次选择。
m_pDataManager->setIncludeOtherWells(bChecked);
fillResultWellCombo(m_pDataManager->getPrimaryWellCode());
fillResultWellCombo();
}
void nmWxNumericalDesign::onWellCollectionChanged()
{
// 这里只刷新多井入口避免Map编辑时覆盖用户尚未提交的其他数值设置。
updateIncludeOtherWellsAvailability();
// 删除井不清除快照;重新填充后保留 UUID 选择并显示“已删除”状态。
fillResultWellCombo();
}
// 当前模型下拉框切换槽,将用户选择的模型类型同步到数据管理器
@ -1135,7 +1126,7 @@ void nmWxNumericalDesign::onOptionsIconClicked()
m_pIncludeOtherWellsCheck->isChecked());
nmWxParameterProperty::notifyUpdateTable();
fillResultWellCombo(m_pDataManager->getPrimaryWellCode());
fillResultWellCombo();
}
}

@ -19,6 +19,7 @@
#include "nmDataOutline.h"
#include "nmDataReservoir.h"
#include "nmDataPvtParaForPebi.h"
#include "nmPebiResultSnapshot.h"
#include "nmAttrRegistry.h"
#include <QCoreApplication>
@ -502,57 +503,59 @@ void nmWxParaPropertyPebi::rebuildResultParas()
if (m_pMainLayout == nullptr || m_pDataManager == nullptr)
return;
m_pDataManager->syncWellAttrs();
QStringList listParas;
QMap<QString, QVariant> mapValues;
QMap<QString, QString> mapObjectNames;
QSet<QString> addedWellCodes;
nmAttrRegistry* pReg = m_pDataManager->getAttrRegistry();
QSharedPointer<const nmPebiResultSnapshot> pSnapshot =
m_pDataManager->getPebiResultSnapshot();
if (pSnapshot.isNull())
{
refreshUIs(listParas, false, mapObjectNames);
return;
}
// 每口井单独建立“模型-井名”分组,只保留该井型传给求解器的参数。
// 每口井单独建立“模型-井名”分组,名称、井型和数值均冻结在快照中
int wellIndex = 0;
const QVector<nmDataWellBase*> wells = m_pDataManager->getWellDataList();
foreach (nmDataWellBase* pWell, wells)
for (int nWellIndex = 0;
nWellIndex < pSnapshot->getWellCount();
++nWellIndex)
{
const nmPebiResultWellSnapshot* pWell =
pSnapshot->getWellAt(nWellIndex);
if (pWell == nullptr)
continue;
const QString sCode = pWell->getWellCode();
if (sCode.isEmpty() || addedWellCodes.contains(sCode))
continue;
const QStringList resultParas = resultWellParasOf(
pWell->getPerforationCount() > 0, pWell->getWellType());
pWell->m_bHasPerforation, pWell->m_eWellType);
if (resultParas.isEmpty())
continue;
const QString groupCode = QString("RESULT_01_WELL_%1")
.arg(++wellIndex, 4, 10, QChar('0'));
addedWellCodes.insert(sCode);
mapObjectNames[groupCode] = resultModelTitlePrefix() + pWell->getWellName();
mapObjectNames[groupCode] =
resultModelTitlePrefix() + pWell->m_sWellName;
QStringList sourceParas;
foreach (const QString& resultPara, resultParas)
foreach (const QString& sResultPara, resultParas)
{
sourceParas << sCode + "_" + resultPara.mid(QString("Result_").length());
}
const QMap<QString, QVariant> sourceValues =
pReg != nullptr ? pReg->collect(sourceParas) : QMap<QString, QVariant>();
for (int i = 0; i < resultParas.size(); ++i)
{
if (sourceValues.contains(sourceParas[i]))
QVariant oValue;
if (sResultPara == "Result_W_Rw")
oValue = pWell->m_dRadius;
else if (sResultPara == "Result_W_Dfc")
oValue = pWell->m_dDfc;
else if (sResultPara == "Result_W_C")
oValue = pWell->m_dWellboreStorage;
else if (sResultPara == "Result_W_Skin")
oValue = pWell->m_dSkin;
if (oValue.isValid())
{
appendResultPara(listParas, mapValues, groupCode,
resultParas[i], sourceValues.value(sourceParas[i]));
sResultPara, oValue);
}
}
}
// 储层分组使用独立结果参数名,避免改变左侧属性面板的别名和单位。
nmDataReservoir* pReservoir = m_pDataManager->getReservoirData();
if (pReservoir != nullptr)
// 储层和 PVT 只读取求解时冻结值,实时参数修改仅令结果变为非当前。
{
const QString groupCode = "RESULT_02_RESERVOIR";
QString reservoirTitle = QCoreApplication::translate(
@ -560,17 +563,19 @@ void nmWxParaPropertyPebi::rebuildResultParas()
reservoirTitle.replace(" - ", "-");
mapObjectNames[groupCode] = reservoirTitle;
const nmPebiResultReservoirParameters& oReservoir =
pSnapshot->getReservoirParameters();
const QStringList resultParas = resultReservoirParas();
const QVariant reservoirValues[] = {
pReservoir->getInitialPressure().getValue(),
pReservoir->getPermeability().getValue(),
pReservoir->getThickness().getValue(),
pReservoir->getPorosity().getValue(),
pReservoir->getCt().getValue(),
pReservoir->getCf().getValue(),
pReservoir->getSoi().getValue(),
pReservoir->getSgi().getValue(),
pReservoir->getSwi().getValue()
oReservoir.m_dInitialPressure,
oReservoir.m_dPermeability,
oReservoir.m_dThickness,
oReservoir.m_dPorosity,
oReservoir.m_dTotalCompressibility,
oReservoir.m_dRockCompressibility,
oReservoir.m_dOilSaturation,
oReservoir.m_dGasSaturation,
oReservoir.m_dWaterSaturation
};
for (int i = 0; i < resultParas.size(); ++i)
{
@ -581,73 +586,59 @@ void nmWxParaPropertyPebi::rebuildResultParas()
// PVT 分组按本次求解模型选择实际提交的油、水或气参数。
const QString pvtGroupCode = "RESULT_03_PVT";
nmDataPvtParaForPebi* pPvt = m_pDataManager->getPebiPvtPara();
if (pReservoir != nullptr)
{
switch (m_pDataManager->getSolverModelType())
{
case SMT_Oil_ConstPvt:
appendResultPara(listParas, mapValues, pvtGroupCode,
"Result_Bo", pReservoir->getBo().getValue());
appendResultPara(listParas, mapValues, pvtGroupCode,
"Result_Miuo", pReservoir->getMiuo().getValue());
break;
case SMT_Water_ConstPvt:
appendResultPara(listParas, mapValues, pvtGroupCode,
"Result_Bw", pReservoir->getBw().getValue());
appendResultPara(listParas, mapValues, pvtGroupCode,
"Result_Miuw", pReservoir->getMiuw().getValue());
break;
case SMT_Oil_VariablePvt:
if (pPvt != nullptr)
{
appendPvtVectorPara(listParas, mapValues, pvtGroupCode,
"Result_Bo", pPvt->getBo());
appendPvtVectorPara(listParas, mapValues, pvtGroupCode,
"Result_Miuo", pPvt->getMiuo());
appendPvtVectorPara(listParas, mapValues, pvtGroupCode,
"Result_Co", pPvt->getCo());
}
break;
case SMT_Water_VariablePvt:
if (pPvt != nullptr)
{
appendPvtVectorPara(listParas, mapValues, pvtGroupCode,
"Result_Bw", pPvt->getBw());
appendPvtVectorPara(listParas, mapValues, pvtGroupCode,
"Result_Miuw", pPvt->getMiuw());
appendPvtVectorPara(listParas, mapValues, pvtGroupCode,
"Result_Cw", pPvt->getCw());
}
break;
case SMT_Gas_VariablePvt:
case SMT_Gas_PseudoPressure:
if (pPvt != nullptr)
{
appendPvtVectorPara(listParas, mapValues, pvtGroupCode,
"Result_Bg", pPvt->getBg());
appendPvtVectorPara(listParas, mapValues, pvtGroupCode,
"Result_Miug", pPvt->getMiug());
appendPvtVectorPara(listParas, mapValues, pvtGroupCode,
"Result_Cg", pPvt->getCg());
}
break;
case SMT_Oil_Water_TwoPhase:
if (pPvt != nullptr)
{
appendPvtVectorPara(listParas, mapValues, pvtGroupCode,
"Result_Bo", pPvt->getBo());
appendPvtVectorPara(listParas, mapValues, pvtGroupCode,
"Result_Miuo", pPvt->getMiuo());
appendPvtVectorPara(listParas, mapValues, pvtGroupCode,
"Result_Bw", pPvt->getBw());
appendPvtVectorPara(listParas, mapValues, pvtGroupCode,
"Result_Miuw", pPvt->getMiuw());
}
break;
default:
break;
}
const nmPebiResultPvtParameters& oPvt =
pSnapshot->getPvtParameters();
switch (pSnapshot->getSolverSettings().m_nSolverModelType)
{
case SMT_Oil_ConstPvt:
appendResultPara(listParas, mapValues, pvtGroupCode,
"Result_Bo", oPvt.m_dConstantBo);
appendResultPara(listParas, mapValues, pvtGroupCode,
"Result_Miuo", oPvt.m_dConstantMiuo);
break;
case SMT_Water_ConstPvt:
appendResultPara(listParas, mapValues, pvtGroupCode,
"Result_Bw", oPvt.m_dConstantBw);
appendResultPara(listParas, mapValues, pvtGroupCode,
"Result_Miuw", oPvt.m_dConstantMiuw);
break;
case SMT_Oil_VariablePvt:
appendPvtVectorPara(listParas, mapValues, pvtGroupCode,
"Result_Bo", oPvt.m_vecBo);
appendPvtVectorPara(listParas, mapValues, pvtGroupCode,
"Result_Miuo", oPvt.m_vecMiuo);
appendPvtVectorPara(listParas, mapValues, pvtGroupCode,
"Result_Co", oPvt.m_vecCo);
break;
case SMT_Water_VariablePvt:
appendPvtVectorPara(listParas, mapValues, pvtGroupCode,
"Result_Bw", oPvt.m_vecBw);
appendPvtVectorPara(listParas, mapValues, pvtGroupCode,
"Result_Miuw", oPvt.m_vecMiuw);
appendPvtVectorPara(listParas, mapValues, pvtGroupCode,
"Result_Cw", oPvt.m_vecCw);
break;
case SMT_Gas_VariablePvt:
case SMT_Gas_PseudoPressure:
appendPvtVectorPara(listParas, mapValues, pvtGroupCode,
"Result_Bg", oPvt.m_vecBg);
appendPvtVectorPara(listParas, mapValues, pvtGroupCode,
"Result_Miug", oPvt.m_vecMiug);
appendPvtVectorPara(listParas, mapValues, pvtGroupCode,
"Result_Cg", oPvt.m_vecCg);
break;
case SMT_Oil_Water_TwoPhase:
appendPvtVectorPara(listParas, mapValues, pvtGroupCode,
"Result_Bo", oPvt.m_vecBo);
appendPvtVectorPara(listParas, mapValues, pvtGroupCode,
"Result_Miuo", oPvt.m_vecMiuo);
appendPvtVectorPara(listParas, mapValues, pvtGroupCode,
"Result_Bw", oPvt.m_vecBw);
appendPvtVectorPara(listParas, mapValues, pvtGroupCode,
"Result_Miuw", oPvt.m_vecMiuw);
break;
default:
break;
}
if (listParas.contains(resultParaName(pvtGroupCode, "Result_Bo")) ||

@ -82,6 +82,8 @@
// 业务逻辑相关的头文件
#include "nmDataAnalyzeManager.h" // 数据管理器
#include "nmPebiResultSnapshot.h"
#include "nmPebiResultSnapshotRenderAdapter.h"
// 数据过滤对话框
#include "nmWxPostprocessingAnimationDataFiltering.h"
@ -158,7 +160,8 @@ nmWxPostprocessingAnimationWidget::nmWxPostprocessingAnimationWidget(
QWidget(pParent),
m_pDataManager(pDataManager != nullptr
? pDataManager
: nmDataAnalyzeManager::getCurrentInstance())
: nmDataAnalyzeManager::getCurrentInstance()),
m_pResultRenderAdapter(new nmPebiResultSnapshotRenderAdapter())
{
// 初始化成员变量
m_nCurrentIndex = 0;
@ -174,7 +177,6 @@ nmWxPostprocessingAnimationWidget::nmWxPostprocessingAnimationWidget(
m_scalarBar = nullptr;
m_lookupTable = nullptr;
m_textActor = nullptr;
m_pCachedBaseGrid = nullptr;
m_thresholdFilter = nullptr;
// 初始化过滤器设置内容
@ -203,6 +205,11 @@ nmWxPostprocessingAnimationWidget::nmWxPostprocessingAnimationWidget(
connect(m_resultDialog, SIGNAL(clearContourRequested()),
this, SLOT(slotClearContour()));
if(!m_pDataManager.isNull()) {
m_pResultSnapshot =
m_pDataManager.data()->getPebiResultSnapshot();
}
// 初始化时间步列表 (在 initUI 之前,因为它会影响 UI 控件的最大值)
this->initTimeSteps();
// 初始化用户界面
@ -212,6 +219,14 @@ nmWxPostprocessingAnimationWidget::nmWxPostprocessingAnimationWidget(
// 析构函数:用于在对象销毁时进行清理
nmWxPostprocessingAnimationWidget::~nmWxPostprocessingAnimationWidget()
{
// 先断开过滤器,再销毁适配器,确保管道不保留旧压力数组和快照引用。
if(m_thresholdFilter != nullptr) {
m_thresholdFilter->SetInputConnection(NULL);
}
delete m_pResultRenderAdapter;
m_pResultRenderAdapter = nullptr;
m_pResultSnapshot.clear();
// Qt 的父子对象机制会自动删除 QVTKWidget 和其他 Qt 控件(如 m_pSlider, m_pProgress, m_pPlayTimer
// vtkSmartPointer 会自动管理其内部 VTK 对象的内存。
// 但为了确保 VTK 渲染器从 RenderWindow 中被正确移除(避免潜在的 VTK 内部引用问题),
@ -255,8 +270,18 @@ void nmWxPostprocessingAnimationWidget::refreshResult()
m_pointPlacer = nullptr;
m_polyDataForContour = nullptr;
m_pCachedBaseGrid = pDataManager->getResultBaseGrid();
m_thresholdFilter->SetInputData(m_pCachedBaseGrid);
// 快照整体替换后先清除适配器,解除旧渲染网格持有的压力帧引用。
m_pResultSnapshot = pDataManager->getPebiResultSnapshot();
m_pResultRenderAdapter->clear();
if(!m_pResultSnapshot.isNull() &&
m_pResultRenderAdapter->initialize(
m_pResultSnapshot,
nmPebiResultSnapshotRenderAdapter::SharedPressureFrame)) {
m_thresholdFilter->SetInputConnection(
m_pResultRenderAdapter->getOutputPort());
} else {
m_thresholdFilter->SetInputConnection(NULL);
}
m_nCurrentIndex = 0;
m_vecTimeStepKeys.clear();
initTimeSteps();
@ -275,20 +300,22 @@ void nmWxPostprocessingAnimationWidget::refreshResult()
: QString("0%(0/0)"));
}
double scalarRange[2];
pDataManager->getScalarRangeP(scalarRange);
double scalarRange[2] = { 0.0, 0.0 };
const bool bHasScalarRange = !m_pResultSnapshot.isNull() &&
m_pResultSnapshot->getScalarRange(scalarRange);
m_mapper->SetScalarRange(scalarRange);
m_lookupTable->SetRange(scalarRange);
m_lookupTable->Build();
m_thresholdFilter->ThresholdBetween(scalarRange[0], scalarRange[1]);
m_thresholdFilter->Modified();
m_bFilteringEnabled = true;
m_bAboveMinEnabled = true;
m_bFilteringEnabled = bHasScalarRange;
m_bAboveMinEnabled = bHasScalarRange;
m_dMinValue = scalarRange[0];
m_bBelowMaxEnabled = true;
m_bBelowMaxEnabled = bHasScalarRange;
m_dMaxValue = scalarRange[1];
if(!m_vecTimeStepKeys.isEmpty() && m_pCachedBaseGrid != nullptr) {
if(!m_vecTimeStepKeys.isEmpty() &&
m_pResultRenderAdapter->getCellCount() > 0) {
loadDataForIndex(0);
}
initWellDrawing();
@ -518,18 +545,17 @@ void nmWxPostprocessingAnimationWidget::initVTKWidget()
m_lookupTable->SetHueRange(0.67, 0); // 设置色调范围(从蓝色到红色)
m_lookupTable->Build(); // 构建颜色表
// 4. 获取基础结果网格数据
nmDataAnalyzeManager* pDataManager = m_pDataManager.data();
if(pDataManager) {
//m_pCachedBaseGrid = pDataManager->getResultBaseGridCopy();
m_pCachedBaseGrid = pDataManager->getResultBaseGrid();
}
// 4. 通过受控适配器建立独立渲染网格;普通 UI 不取得快照 VTK 指针。
const bool bResultReady = !m_pResultSnapshot.isNull() &&
m_pResultRenderAdapter->initialize(
m_pResultSnapshot,
nmPebiResultSnapshotRenderAdapter::SharedPressureFrame);
// 5. 创建阈值过滤器和mapper并设置输入数据
m_thresholdFilter = vtkSmartPointer<vtkThreshold>::New();
if(m_pCachedBaseGrid) {
m_thresholdFilter->SetInputData(m_pCachedBaseGrid);
if(bResultReady) {
m_thresholdFilter->SetInputConnection(
m_pResultRenderAdapter->getOutputPort());
}
m_thresholdFilter->SetInputArrayToProcess(0, 0, 0, vtkDataObject::FIELD_ASSOCIATION_CELLS, "p");
// 上面这行是关键!它告诉过滤器对哪个数组进行操作,
@ -589,8 +615,9 @@ void nmWxPostprocessingAnimationWidget::initVTKWidget()
//10. 初始渲染
// 设置压力标量范围
double scalarRange[2];
pDataManager->getScalarRangeP(scalarRange);
double scalarRange[2] = { 0.0, 0.0 };
const bool bHasScalarRange = bResultReady &&
m_pResultSnapshot->getScalarRange(scalarRange);
m_mapper->SetScalarRange(scalarRange);
m_lookupTable->SetRange(scalarRange);
@ -602,10 +629,10 @@ void nmWxPostprocessingAnimationWidget::initVTKWidget()
m_thresholdFilter->Modified(); // 通知过滤器其参数已修改
// 修改过滤设置
m_bFilteringEnabled = true;
m_bAboveMinEnabled = true;
m_bFilteringEnabled = bHasScalarRange;
m_bAboveMinEnabled = bHasScalarRange;
m_dMinValue = scalarRange[0];
m_bBelowMaxEnabled = true;
m_bBelowMaxEnabled = bHasScalarRange;
m_dMaxValue = scalarRange[1];
// // 创建自定义交互样式
@ -635,22 +662,23 @@ void nmWxPostprocessingAnimationWidget::initWellDrawing()
{
clearWellActors(); // 确保每次初始化时清空旧的 Actor避免重复添加
nmDataAnalyzeManager* pDataManager = m_pDataManager.data();
if(!pDataManager) {
if(m_pResultSnapshot.isNull()) {
return;
}
// 获取所有井的二维位置信息,映射键为 WellCode。
QMap<QString, QPointF> mapWellLocations = pDataManager->getAllWellLocations();
// 遍历所有井,创建并配置它们的 3D 文本和线 Actor
for(auto it = mapWellLocations.constBegin(); it != mapWellLocations.constEnd(); ++it) {
const QString& sWellCode = it.key();
const QPointF& ptLocation = it.value(); // 井的二维位置 (X, Y)
nmDataWellBase* pWellData = pDataManager->findWellByCode(sWellCode);
const QString sWellName = pWellData != nullptr
? pWellData->getWellName() : sWellCode;
// 遍历快照井,实时井删除、改名或移动都不会改变历史场图标注。
for(int nWellIndex = 0;
nWellIndex < m_pResultSnapshot->getWellCount();
++nWellIndex) {
const nmPebiResultWellSnapshot* pWell =
m_pResultSnapshot->getWellAt(nWellIndex);
if(pWell == NULL) {
continue;
}
const QString& sWellInstanceId = pWell->m_sWellInstanceId;
const QPointF& ptLocation = pWell->m_oLocation;
const QString sWellName = pWell->m_sWellName.isEmpty()
? pWell->m_sWellCode : pWell->m_sWellName;
// === 1. 创建井名称的 3D 文本 Actor ===
// 使用 vtkBillboardTextActor3D它始终面向相机文字可读性最好且是 3D 对象。
@ -666,7 +694,7 @@ void nmWxPostprocessingAnimationWidget::initWellDrawing()
textActor->SetVisibility(m_bShowWellActors ? 1 : 0);
m_renderer->AddActor(textActor); // 对于所有 3D Actor都使用 m_renderer->AddActor()
m_mapWellNameActors.insert(sWellCode, textActor); // Actor内部按WellCode管理文本仍显示井名
m_mapWellNameActors.insert(sWellInstanceId, textActor);
// === 2. 绘制井的线 (为每口井绘制,基于 wellLocations 的 X, Y并延伸到 WELL_DRAWING_Z_HEIGHT) ===
// 创建点集合
@ -702,8 +730,8 @@ void nmWxPostprocessingAnimationWidget::initWellDrawing()
lineActor->SetVisibility(m_bShowWellActors ? 1 : 0);
m_renderer->AddActor(lineActor); // 将 3D Actor 添加到渲染器
// 井名允许修改或重复,井线 Actor 必须和其余求解结果一样按 WellCode 管理
m_mapWellLineActors.insert(sWellCode, lineActor);
// Actor 同样按求解时 UUID 管理,不能被后来创建的同名或同编码井接管
m_mapWellLineActors.insert(sWellInstanceId, lineActor);
}
// 强制渲染以显示新增的 Actor
@ -834,51 +862,37 @@ void nmWxPostprocessingAnimationWidget::initOperPannel()
m_pPlayTimer->setInterval(50); // 设置定时器间隔为 50 毫秒(即每 50 毫秒更新一帧)
}
// 初始化时间步列表,从 nmDataAnalyzeManager 单例获取数据
// 初始化时间步列表,只读取当前窗口持有的快照
void nmWxPostprocessingAnimationWidget::initTimeSteps()
{
nmDataAnalyzeManager* pDataManager = m_pDataManager.data();
if(!pDataManager) {
m_vecTimeStepKeys.clear();
if(m_pResultSnapshot.isNull()) {
return;
}
// 通过公共方法获取所有时间步的键(时间戳)列表
QList<double> keys = pDataManager->getTimeStepKeys();
// 对时间步进行排序,确保动画播放顺序是正确的(从小到大)
std::sort(keys.begin(), keys.end());
m_vecTimeStepKeys = QVector<double>::fromList(keys); // 将 QList 转换为 QVector 存储
// Builder 已校验时间严格递增,保持快照下标与适配器帧下标一一对应。
m_vecTimeStepKeys.reserve(m_pResultSnapshot->getPressureFrameCount());
for(int nFrameIndex = 0;
nFrameIndex < m_pResultSnapshot->getPressureFrameCount();
++nFrameIndex) {
m_vecTimeStepKeys.append(
m_pResultSnapshot->getPressureTimeAt(nFrameIndex));
}
}
// 根据指定索引加载并渲染一个时间步的数据
void nmWxPostprocessingAnimationWidget::loadDataForIndex(int index)
{
// 确保索引在有效范围内
if(index < 0 || index >= m_vecTimeStepKeys.size() || !m_pCachedBaseGrid) {
if(index < 0 || index >= m_vecTimeStepKeys.size() ||
m_pResultRenderAdapter == nullptr) {
return;
}
nmDataAnalyzeManager* pDataManager = m_pDataManager.data();
if(!pDataManager) return;
// 2. 获取当前时间步对应的压力数据
double currentTime = m_vecTimeStepKeys[index]; // 获取当前时间步的时间戳
vtkSmartPointer<vtkDoubleArray> currentPressureData = pDataManager->getTimeStepData(
currentTime); // 获取对应的压力数据
if(!currentPressureData) return;
const char* arrayName = currentPressureData->GetName();
if (arrayName) {
qDebug() << "Loaded data array name is:" << arrayName;
} else {
qDebug() << "Loaded data array has no name.";
}
// 3. 将压力数据设置到PEBI单元数据中
m_pCachedBaseGrid->GetCellData()->SetScalars(currentPressureData);
// 适配器负责共享或复制压力帧UI 不取得可写 vtkDoubleArray 指针。
if(!m_pResultRenderAdapter->setPressureFrame(index)) {
return;
}
m_thresholdFilter->SetInputArrayToProcess(0, 0, 0, vtkDataObject::FIELD_ASSOCIATION_CELLS, "p");
// 更新时间文本演员显示的时间戳
@ -1160,7 +1174,8 @@ QImage nmWxPostprocessingAnimationWidget::getVTKRenderWindowAsImage()
void nmWxPostprocessingAnimationWidget::exportAnimationAsVideo()
{
// 检查动画数据和渲染窗口是否可用,如果不可用则弹出警告并退出。
if(m_vecTimeStepKeys.isEmpty() || !m_pVtkWidget || !m_pVtkWidget->GetRenderWindow()) {
if(m_vecTimeStepKeys.isEmpty() || m_pResultSnapshot.isNull() ||
!m_pVtkWidget || !m_pVtkWidget->GetRenderWindow()) {
QMessageBox::warning(this, tr("Warning"), tr("No animation data or render window available."));
return;
}
@ -1191,6 +1206,17 @@ void nmWxPostprocessingAnimationWidget::exportAnimationAsVideo()
// 停止正在进行的动画播放,避免与导出过程冲突。
on_stop();
// 离屏导出使用单帧复制缓冲,避免不受控导出管道持有或修改规范压力数组。
nmPebiResultSnapshotRenderAdapter oExportAdapter;
if(!oExportAdapter.initialize(
m_pResultSnapshot,
nmPebiResultSnapshotRenderAdapter::CopiedPressureFrame)) {
if(wasPlaying) on_start();
QMessageBox::warning(this, tr("Warning"),
tr("Cannot prepare animation result data."));
return;
}
// 1. 创建离屏渲染管线
// 创建一个VTK渲染窗口但设置为离屏渲染这意味着它不会在屏幕上显示。
vtkSmartPointer<vtkRenderWindow> offscreenRenderWindow = vtkSmartPointer<vtkRenderWindow>::New();
@ -1202,7 +1228,7 @@ void nmWxPostprocessingAnimationWidget::exportAnimationAsVideo()
// 将渲染器添加到离屏渲染窗口中。
offscreenRenderWindow->AddRenderer(offscreenRenderer);
// 2. 深度复制所有渲染内容 (兼容 VTK 7.1)
// 2. 复制轻量渲染状态;基础网格结构由适配器受控共享,不做整网格 DeepCopy。
// 复制相机:从主渲染器的相机深度复制到离屏渲染器,确保视角一致。
vtkSmartPointer<vtkCamera> camera = vtkSmartPointer<vtkCamera>::New();
@ -1217,19 +1243,12 @@ void nmWxPostprocessingAnimationWidget::exportAnimationAsVideo()
offscreenRenderer->SetBackground2(background2);
offscreenRenderer->SetGradientBackground(m_renderer->GetGradientBackground());
// 复制基础数据对象:深度复制网格数据,确保离屏渲染使用独立的数据副本。
vtkSmartPointer<vtkUnstructuredGrid> offscreenGrid = vtkSmartPointer<vtkUnstructuredGrid>::New();
if(m_pCachedBaseGrid) {
offscreenGrid->DeepCopy(m_pCachedBaseGrid);
}
// 复制主 Actor 的 Mapper、Actor 和 ScalarBar (手动复制属性)
// 复制 Mapper创建一个新的Mapper并设置其输入数据和颜色映射表等属性。
vtkSmartPointer<vtkDataSetMapper> offscreenMapper = vtkSmartPointer<vtkDataSetMapper>::New();
if(m_mapper) {
offscreenMapper->SetInputData(offscreenGrid);
offscreenMapper->SetInputConnection(oExportAdapter.getOutputPort());
offscreenMapper->SetLookupTable(m_mapper->GetLookupTable());
offscreenMapper->ScalarVisibilityOn();
offscreenMapper->SetScalarRange(m_mapper->GetScalarRange());
@ -1377,24 +1396,12 @@ void nmWxPostprocessingAnimationWidget::exportAnimationAsVideo()
return;
}
// 获取数据管理器实例。
nmDataAnalyzeManager* pDataManager = m_pDataManager.data();
if(pDataManager) {
// 获取当前时间步的数据。
double currentTime = m_vecTimeStepKeys[i];
vtkSmartPointer<vtkDoubleArray> currentPressureData = pDataManager->getTimeStepData(currentTime);
if(currentPressureData) {
// 将数据设置到离屏网格中,并标记数据已修改。
vtkDataSet* dataSet = vtkDataSet::SafeDownCast(offscreenGrid);
if(dataSet) {
dataSet->GetCellData()->SetScalars(currentPressureData);
dataSet->Modified();
}
}
if(!oExportAdapter.setPressureFrame(i)) {
pVideoWriter->End();
if(wasPlaying) on_start();
QMessageBox::warning(this, tr("Warning"),
tr("Cannot load an animation frame."));
return;
}
// 渲染离屏窗口。

Loading…
Cancel
Save