diff --git a/Bin/Config/Lang/cn/nmNum_cn.qm b/Bin/Config/Lang/cn/nmNum_cn.qm index 461b8f77..e7a2d926 100644 Binary files a/Bin/Config/Lang/cn/nmNum_cn.qm and b/Bin/Config/Lang/cn/nmNum_cn.qm differ diff --git a/Bin/Config/Lang/cn/nmNum_cn.ts b/Bin/Config/Lang/cn/nmNum_cn.ts index d84bb1c5..9659e0c4 100644 --- a/Bin/Config/Lang/cn/nmNum_cn.ts +++ b/Bin/Config/Lang/cn/nmNum_cn.ts @@ -2883,6 +2883,19 @@ Reason: %1 Loop enabled启用循环 Speed (steps/second)速度(步/秒) + + nmWxPebiResultViewWidget + No result data.暂无结果数据。 + Result Grid结果网格 + Color Scale色标 + Scale Grid比例网格 + Cell Edges单元边线 + Well Markers井位标记 + Well Names井名 + Unable to display the latest result.无法显示最新结果。 + Unable to display the latest result: %1无法显示最新结果:%1 + Current inputs have changed. Showing the last successful result.当前输入已修改,正在显示上一次成功求解的结果。 + nmWxPebiGridViewWidget No grid data.暂无网格数据。 diff --git a/Include/nmNum/nmData/nmDataAnalyzeManager.h b/Include/nmNum/nmData/nmDataAnalyzeManager.h index 53912c20..33e472ed 100644 --- a/Include/nmNum/nmData/nmDataAnalyzeManager.h +++ b/Include/nmNum/nmData/nmDataAnalyzeManager.h @@ -565,9 +565,15 @@ public: void markPebiGridBuilt(); /** @brief 仅在后台划分期间输入未变化时登记网格成功。 */ bool markPebiGridBuiltIfCurrent(quint64 nGridInputRevision); - /** @brief 几何或求解井集合变化后,使 PEBI 网格和结果失效。 */ + /** + * @brief 几何或求解井集合变化后,使 PEBI 网格和结果失效。 + * @note 只能在 Manager 所属线程调用;Release 下越线程请求会被拒绝。 + */ void invalidatePebiGrid(); - /** @brief 产量、PVT 等变化后,仅使 PEBI 结果失效。 */ + /** + * @brief 产量、PVT 等变化后,仅使 PEBI 结果失效。 + * @note 只能在 Manager 所属线程调用;Release 下越线程请求会被拒绝。 + */ void invalidatePebiResults(); /** @brief 判断当前 PEBI 网格是否与最新输入一致。 */ bool isPebiGridValid() const; @@ -786,12 +792,34 @@ public: bool bSucceeded); /** @brief 并行结果快照或其结果井选择已经改变。 */ void sigPebiResultSnapshotChanged(); + /** + * @brief 结果快照身份、存在性或相对当前输入的新鲜度已经改变。 + * @note 当前结果井选择不发送本信号;新结果视图只监听本信号。 + */ + void sigPebiResultStateChanged(); private slots: /** @brief 根据参数面板实际编辑的属性类型,使对应 PEBI 输入版本失效。 */ void onRegisteredAttributeEdited(QString sName, QVariant oValue); private: + /** @brief 一次 Manager 修改前捕获的可观察结果状态。 */ + struct PebiResultStateSnapshot + { + QSharedPointer m_pSnapshot; + QString m_sSnapshotId; + bool m_bCurrent; + }; + + /** @brief 断言并在 Release 中拒绝越线程的 PEBI 结果状态修改。 */ + bool ensurePebiResultOwnerThread(const char* pOperation) const; + /** @brief 在已经确认属于 Manager 的线程中发送结果状态通知。 */ + void emitPebiResultStateChangedOnOwnerThread(); + /** @brief 捕获快照共享身份、稳定 ID 和相对输入版本的新鲜度。 */ + PebiResultStateSnapshot capturePebiResultState() const; + /** @brief 仅在修改前后的可观察结果状态确实不同时发送新状态信号。 */ + void notifyPebiResultStateChangedIfNeeded( + const PebiResultStateSnapshot& oPreviousState); /** @brief 按 UUID 给当前实时井统一绑定或解除最后结果快照弱引用。 */ void rebindPebiResultSnapshotToWells(); /** @brief 在候选 Manager 内完整读取并校验一套 v5 成果。 */ diff --git a/Include/nmNum/nmSubWnd/nmSubWndPostprocessing.h b/Include/nmNum/nmSubWnd/nmSubWndPostprocessing.h index 747c5eb4..8e06b720 100644 --- a/Include/nmNum/nmSubWnd/nmSubWndPostprocessing.h +++ b/Include/nmNum/nmSubWnd/nmSubWndPostprocessing.h @@ -3,16 +3,29 @@ #include "iSubWndBaseFit.h" #include "nmSubWnd_global.h" +#include + class nmDataDemo; +class nmDataAnalyzeManager; +class nmWxPebiResultViewWidget; class NM_SUB_WND_EXPORT nmSubWndPostprocessing : public iSubWndBaseFit { Q_OBJECT public: - explicit nmSubWndPostprocessing(QWidget *parent = 0, QString sExt = ""); + /** @brief 显式绑定创建窗口时的分析 Manager;空指针创建稳定空结果窗口。 */ + explicit nmSubWndPostprocessing( + nmDataAnalyzeManager* pDataManager, + QWidget *parent = 0, + QString sExt = ""); ~nmSubWndPostprocessing(); + /** @brief 切换独立窗口观察的 Manager,并立即转发给已创建的结果控件。 */ + void setDataManager(nmDataAnalyzeManager* pDataManager); + /** @brief 返回当前显式绑定的 Manager;对象销毁后自动返回空指针。 */ + nmDataAnalyzeManager* getDataManager() const; + // 配置窗体 virtual void configWnd(QString sPrevInfo); @@ -23,4 +36,10 @@ public: public: // 执行命令:通常来自Ribbon virtual bool runCmdBy(QString sName, QString sID); + +private: + /** @brief 独立窗口只观察显式注入的 Manager,不回退到全局当前实例。 */ + QPointer m_pDataManager; + /** @brief 主界面中的结果组合控件,由 Qt 窗口对象树拥有。 */ + QPointer m_pResultView; }; diff --git a/Include/nmNum/nmSubWnd/nmSubWndUtils.h b/Include/nmNum/nmSubWnd/nmSubWndUtils.h index 98281c49..86afec72 100644 --- a/Include/nmNum/nmSubWnd/nmSubWndUtils.h +++ b/Include/nmNum/nmSubWnd/nmSubWndUtils.h @@ -133,6 +133,14 @@ public: QString sRstName, \ const ZxMainWindow* pMainWnd); + /** + * @brief 统一安装或同步 PEBI 三维结果视图和只读结果参数页。 + * @note 不要求当前已有快照;空快照会清空两个页面中的旧结果内容。 + */ + static bool ensurePebiResultWidgets( + iSubWndFitting* pSubWndFit, + nmDataAnalyzeManager* pDataManager); + /// @brief 获取缓存的TabWidget指针,确保弹窗与MDI切换使用同一个容器,指针比较才能正确 ZxTabWidget* getTabWidget() const { return m_pTabWx; } diff --git a/Include/nmNum/nmSubWxs/nmPebiResultPlaybackController.h b/Include/nmNum/nmSubWxs/nmPebiResultPlaybackController.h index 3e3022a4..771a4552 100644 --- a/Include/nmNum/nmSubWxs/nmPebiResultPlaybackController.h +++ b/Include/nmNum/nmSubWxs/nmPebiResultPlaybackController.h @@ -6,6 +6,7 @@ #include #include #include +#include class QTimer; class nmPebiResultSceneController; @@ -37,7 +38,7 @@ public: virtual ~nmPebiResultPlaybackController(); /** - * @brief 按 Snapshot ID 同步场景状态。 + * @brief 按 Scene 成功绑定序号同步场景状态。 * * 新快照和无快照会停止播放;同快照普通重同步保留有效播放状态。 */ @@ -70,6 +71,8 @@ public: bool isAvailable() const; /** @brief 返回控制器最近一次同步到的快照 ID。 */ QString getObservedSnapshotId() const; + /** @brief 返回最近一次普通 Scene 同步观察到的成功绑定序号。 */ + quint64 getObservedBindingRevision() const; /** @brief 直接转发 Scene 中按 Catalog 排序的可用属性 ID。 */ QStringList getAvailablePropertyIds() const; /** @brief 直接转发 Scene 已成功渲染的当前属性 ID。 */ @@ -127,6 +130,10 @@ private: QTimer* m_pTimer; /** @brief 最近一次同步成功的 Snapshot ID,用于识别快照替换。 */ QString m_sObservedSnapshotId; + /** @brief 最近一次普通 Scene 同步观察到的成功绑定序号。 */ + quint64 m_nObservedBindingRevision; + /** @brief 当前活动 Timer 启动时捕获的 Scene 成功绑定序号。 */ + quint64 m_nPlaybackBindingRevision; /** @brief 同一快照内最后一次成功提交的 pressure 零基帧。 */ int m_nLastPressureFrameIndex; /** @brief 当前窗口播放速度,单位为步/秒。 */ diff --git a/Include/nmNum/nmSubWxs/nmPebiResultSceneController.h b/Include/nmNum/nmSubWxs/nmPebiResultSceneController.h index 40abdcb1..e5791842 100644 --- a/Include/nmNum/nmSubWxs/nmPebiResultSceneController.h +++ b/Include/nmNum/nmSubWxs/nmPebiResultSceneController.h @@ -7,6 +7,7 @@ #include #include #include +#include class nmPebiResultSceneControllerPrivate; class nmPebiResultSnapshot; @@ -52,11 +53,13 @@ public: /** * @brief 事务式提交当前属性的颜色、范围、比例和越界颜色设置。 + * @param nExpectedBindingRevision 对话框打开时记录的成功绑定序号。 * @param sExpectedSnapshotId 对话框打开时记录的快照 ID。 * @param sExpectedPropertyId 对话框打开时记录的属性 ID。 * @note 身份或设置校验失败时不修改场景且不 Render;相同设置直接成功。 */ bool applyCurrentDisplaySettings( + quint64 nExpectedBindingRevision, const QString& sExpectedSnapshotId, const QString& sExpectedPropertyId, const nmPebiResultDisplaySettings& oSettings, @@ -77,6 +80,11 @@ public: /** @brief 返回当前是否持有可用快照且稳定结果图层仍完整。 */ bool hasSnapshot() const; + /** + * @brief 返回本窗口最近一次成功提交不同快照对象后的单调绑定序号。 + * @note 初始值为 0;clear、失败和相同对象重复绑定均不改变该值。 + */ + quint64 getSnapshotBindingRevision() const; /** @brief 返回当前快照稳定 ID;无可用快照时返回空字符串。 */ QString getCurrentSnapshotId() const; /** @brief 按 Catalog 顺序返回当前快照真实存在的属性 ID。 */ diff --git a/Include/nmNum/nmSubWxs/nmWxPebiResultColorScaleSettingsDlg.h b/Include/nmNum/nmSubWxs/nmWxPebiResultColorScaleSettingsDlg.h index d676fd38..02e1ed83 100644 --- a/Include/nmNum/nmSubWxs/nmWxPebiResultColorScaleSettingsDlg.h +++ b/Include/nmNum/nmSubWxs/nmWxPebiResultColorScaleSettingsDlg.h @@ -6,6 +6,7 @@ #include #include +#include class QCheckBox; class QComboBox; @@ -18,8 +19,9 @@ class nmPebiResultSceneController; /** * @brief Saphir 风格的当前结果属性颜色设置对话框。 * - * 对话框保存打开时的 Snapshot ID 和 Property ID,每次 Apply/OK 都交给 - * SceneController 原子复核。Cancel 只丢弃尚未 Apply 的编辑,不回滚已提交状态。 + * 对话框保存打开时的绑定序号、Snapshot ID 和 Property ID,每次 Apply/OK + * 都交给 SceneController 原子复核。Cancel 只丢弃尚未 Apply 的编辑, + * 不回滚已经提交的状态。 */ class NM_SUB_WXS_EXPORT nmWxPebiResultColorScaleSettingsDlg : public QDialog @@ -90,6 +92,7 @@ private: void setErrorText(const QString& sError); nmPebiResultSceneController* m_pSceneController; ///< 非所有权 Scene 真源。 + quint64 m_nExpectedBindingRevision; ///< 打开时捕获的 Scene 成功绑定序号。 QString m_sExpectedSnapshotId; ///< 打开时捕获的快照稳定 ID。 QString m_sExpectedPropertyId; ///< 打开时捕获的属性稳定 ID。 QString m_sDisplayUnit; ///< 当前属性默认显示单位,仅用于标签。 diff --git a/Include/nmNum/nmSubWxs/nmWxPebiResultControlBar.h b/Include/nmNum/nmSubWxs/nmWxPebiResultControlBar.h index 1c73c9e6..a519df4e 100644 --- a/Include/nmNum/nmSubWxs/nmWxPebiResultControlBar.h +++ b/Include/nmNum/nmSubWxs/nmWxPebiResultControlBar.h @@ -5,6 +5,7 @@ #include #include +#include class QComboBox; class QToolButton; @@ -42,6 +43,8 @@ public: nmPebiResultPlaybackController* getPlaybackController() const; signals: + /** @brief 一次明确的用户操作已经成功提交,供组合窗口清除旧普通错误。 */ + void operationSucceeded(); /** @brief 转发回放控制器错误,供后续组合窗口显示非阻塞提示。 */ void operationFailed(const QString& sError); @@ -110,6 +113,8 @@ private: QToolButton* m_pSettingsButton; /** @brief 当前属性项对应的 Snapshot ID,仅用于避免重复重建 UI。 */ QString m_sDisplayedSnapshotId; + /** @brief 属性和时间列表最近一次完整展示的 Scene 成功绑定序号。 */ + quint64 m_nDisplayedBindingRevision; /** @brief 当前属性框已展示的有序 ID,仅用于避免重复重建 UI。 */ QStringList m_listDisplayedPropertyIds; /** @brief 当前时间框对应的动态属性 ID。 */ diff --git a/Include/nmNum/nmSubWxs/nmWxPebiResultViewWidget.h b/Include/nmNum/nmSubWxs/nmWxPebiResultViewWidget.h new file mode 100644 index 00000000..cde79d00 --- /dev/null +++ b/Include/nmNum/nmSubWxs/nmWxPebiResultViewWidget.h @@ -0,0 +1,134 @@ +#ifndef NMWXPEBIRESULTVIEWWIDGET_H +#define NMWXPEBIRESULTVIEWWIDGET_H + +#include "nmSubWxs_global.h" + +#include +#include +#include +#include + +class QAction; +class QLabel; +class QMenu; +class QToolBar; + +class nmDataAnalyzeManager; +class nmPebiResultSceneController; +class nmPebiResultSnapshot; +class nmWxPebiResultControlBar; +class nmWxVTKRenderContainerWidget; + +/** + * @brief 独立的 PEBI 结果场景组合窗口。 + * + * 本控件显式观察 DataManager,只组合通用渲染容器、结果 SceneController、 + * 底部控制条和右侧 QAction。Manager 当前状态与 Scene 最后成功显示状态分别 + * 缓存,因此新快照绑定失败时可以继续检查旧画面。 + */ +class NM_SUB_WXS_EXPORT nmWxPebiResultViewWidget : public QWidget +{ + Q_OBJECT + +public: + /** + * @param pDataManager 非所有权对象,可为空;销毁后窗口自动进入空状态。 + * @note 非空 Manager 必须与本窗口同属 Qt GUI 线程。 + */ + explicit nmWxPebiResultViewWidget( + nmDataAnalyzeManager* pDataManager, + QWidget* pParent = nullptr); + /** @brief 按控制条、SceneController、渲染容器的顺序解除窗口资源。 */ + virtual ~nmWxPebiResultViewWidget(); + + /** + * @brief 切换显式 Manager;相同指针只同步,不清空当前相机和场景。 + * @note 跨线程 Manager 会被拒绝,避免同步读取快照引用时发生数据竞争。 + */ + void setDataManager(nmDataAnalyzeManager* pDataManager); + /** @brief 返回当前观察的 Manager;对象销毁后自动返回空指针。 */ + nmDataAnalyzeManager* getDataManager() const; + /** @brief 合并重入请求并读取 Manager 最新结果状态。 */ + void synchronizeFromDataManager(); + +private slots: + /** @brief 响应快照身份、存在性或新鲜度变化,不响应结果井选择。 */ + void onPebiResultStateChanged(); + /** @brief Manager 析构时不再读取其成员,直接停止播放并清理场景。 */ + void onDataManagerDestroyed(QObject* pObject); + /** @brief 将业务显隐 QAction 追加到通用右键菜单。 */ + void appendContextMenuActions(QMenu* pMenu); + /** @brief 转发非阻塞操作错误,但不覆盖绑定失败或过期提示。 */ + void onControlOperationFailed(const QString& sError); + /** @brief 明确操作成功后清除旧普通错误,不影响绑定失败或过期状态。 */ + void onControlOperationSucceeded(); + + /** @brief 切换结果网格显隐。 */ + void onMeshToggled(bool bVisible); + /** @brief 切换色标显隐。 */ + void onScalarBarToggled(bool bVisible); + /** @brief 切换比例网格显隐。 */ + void onScaleGridToggled(bool bVisible); + /** @brief 切换单元边线显隐。 */ + void onCellEdgesToggled(bool bVisible); + /** @brief 切换二维井位标记显隐。 */ + void onWellMarkersToggled(bool bVisible); + /** @brief 切换二维井名显隐。 */ + void onWellNamesToggled(bool bVisible); + +private: + Q_DISABLE_COPY(nmWxPebiResultViewWidget) + + /** @brief 创建唯一渲染容器、SceneController、工具栏、提示栏和控制条。 */ + void createUi(); + /** @brief 执行一次非重入的 Manager 最新状态读取和事务提交。 */ + void synchronizeOnce(); + /** @brief 停止播放、清理 Scene,并把所有业务控件同步为空状态。 */ + void clearSceneAndControls(const QString& sEmptyMessage); + /** @brief 保存本轮已经观察到的 Manager 快照身份和新鲜度。 */ + void rememberObservedState( + const QSharedPointer& pSnapshot, + const QString& sSnapshotId, + bool bCurrent); + /** @brief 从 SceneController 镜像六个业务 QAction 的勾选和可用状态。 */ + void synchronizeBusinessActions(); + /** @brief 根据“绑定失败、过期、操作错误”的固定优先级刷新提示栏。 */ + void refreshStatusBar(); + /** @brief 断开当前 Manager 的两个连接,不访问已经析构的对象。 */ + void disconnectDataManager(); + + QPointer m_pDataManager; ///< 当前显式注入的 Manager。 + nmWxVTKRenderContainerWidget* m_pRenderContainer; ///< 唯一通用 VTK 容器。 + nmPebiResultSceneController* m_pSceneController; ///< 唯一结果场景控制器。 + nmWxPebiResultControlBar* m_pControlBar; ///< 独占回放控制器和 Timer 的底栏。 + QToolBar* m_pToolBar; ///< Qt 父对象拥有的右侧竖向工具栏。 + QLabel* m_pStatusLabel; ///< 过期或错误的非阻塞提示栏。 + + QAction* m_pMeshAction; ///< 结果网格显隐动作。 + QAction* m_pScalarBarAction; ///< 色标显隐动作。 + QAction* m_pScaleGridAction; ///< 比例网格显隐动作。 + QAction* m_pCellEdgesAction; ///< 单元边线显隐动作。 + QAction* m_pWellMarkersAction; ///< 二维井位标记显隐动作。 + QAction* m_pWellNamesAction; ///< 二维井名显隐动作。 + + /** @brief Manager 最近一次通知后观察到的快照强引用。 */ + QSharedPointer m_pObservedSnapshot; + /** @brief Manager 最近一次通知后观察到的稳定 Snapshot ID。 */ + QString m_sObservedSnapshotId; + /** @brief 最近观察到的快照是否仍与当前输入版本一致。 */ + bool m_bObservedSnapshotCurrent; + /** @brief 是否已经建立过一份可用于去重的 Manager 观察状态。 */ + bool m_bObservedStateValid; + + /** @brief Scene 最后一次成功绑定的快照强引用,与观察状态相互独立。 */ + QSharedPointer m_pDisplayedSnapshot; + /** @brief Scene 最后一次成功绑定的稳定 Snapshot ID。 */ + QString m_sDisplayedSnapshotId; + + QString m_sBindingError; ///< 最新 Manager 快照绑定失败的高优先级错误。 + QString m_sOperationError; ///< 当前控制条普通操作错误。 + bool m_bSynchronizing; ///< 防止 VTK 提交期间递归进入同步。 + bool m_bSyncPending; ///< 重入时记录一次待处理请求,结束后循环读取最新状态。 +}; + +#endif // NMWXPEBIRESULTVIEWWIDGET_H diff --git a/Src/nmNum/nmData/nmDataAnalyzeManager.cpp b/Src/nmNum/nmData/nmDataAnalyzeManager.cpp index 8f72dd08..c8572862 100644 --- a/Src/nmNum/nmData/nmDataAnalyzeManager.cpp +++ b/Src/nmNum/nmData/nmDataAnalyzeManager.cpp @@ -1529,6 +1529,9 @@ void nmDataAnalyzeManager::endBackgroundUse() nmDataWellBase *nmDataAnalyzeManager::createWell(NM_WELL_MODEL eWellType) { + if(!ensurePebiResultOwnerThread("createWell")) { + return nullptr; + } // 根据当前Fitting窗口来获取对应的井相关数据 iSubWndFitting* pSubWndFitting = nmDataAnalyzeManager::getCurrentFitting(); nmDataAnalyzeContextProvider* pContextProvider = nmDataAnalyzeContext::provider(); @@ -1571,7 +1574,7 @@ nmDataWellBase *nmDataAnalyzeManager::createWell(NM_WELL_MODEL eWellType) // Map 中增加真实井会改变井约束点和求解器井槽位,旧网格必须立即失效。 m_vWellData.append(pWellData); - m_oNumericalAnalysisCase.invalidateGrid(); + invalidatePebiGrid(); emit dataChanged(); return pWellData; } @@ -1658,6 +1661,9 @@ bool nmDataAnalyzeManager::canRemoveWell( bool nmDataAnalyzeManager::removeWell(nmDataWellBase* pWellData) { + if(!ensurePebiResultOwnerThread("removeWell")) { + return false; + } // 固定主井及非法指针必须在任何版本递增、图元解绑和对象删除前拒绝。 // 该检查是数据层最终兜底,不能只依赖 Map 界面的提前拦截。 if(!canRemoveWell(pWellData)) { @@ -1670,7 +1676,7 @@ bool nmDataAnalyzeManager::removeWell(nmDataWellBase* pWellData) // 第二步:Map 中删除任意真实井都会改变网格井集合。即使它是自动参与的 // 无产量观察井,也必须让旧网格、旧求解器顺序和旧结果一起失效。 - m_oNumericalAnalysisCase.invalidateGrid(); + invalidatePebiGrid(); // 第三步:删除井对象前,先从“包含其他井”中移除对应 WellCode。 QVector vecIncludedWells = @@ -2006,6 +2012,9 @@ nmDataWellBase* nmDataAnalyzeManager::findWellByInstanceId( void nmDataAnalyzeManager::initCurWellData() { + if(!ensurePebiResultOwnerThread("initCurWellData")) { + return; + } // 获取当前默认井数据 ZxDataWell* pWellData = zxCurWell; if(pWellData == nullptr) { @@ -2178,12 +2187,15 @@ void nmDataAnalyzeManager::initCurWellData() if(m_oNumericalAnalysisCase.getPrimaryWellCode().isEmpty() && m_pCurDataWell != nullptr && !m_pCurDataWell->getWellCode().isEmpty()) { + const PebiResultStateSnapshot oPreviousResultState = + capturePebiResultState(); const QString sPrimaryWellCode = m_pCurDataWell->getWellCode(); m_oNumericalAnalysisCase.setPrimaryWellCode(sPrimaryWellCode); m_oNumericalAnalysisCase.setPrimaryWellMode( m_pCurDataWell->getFlowSegmentCount() > 0 ? NM_CaseWell_RateControlled : NM_CaseWell_Observation); + notifyPebiResultStateChangedIfNeeded(oPreviousResultState); } } @@ -2428,6 +2440,9 @@ nmDataWellBase* nmDataAnalyzeManager::appendWellData(ZxDataWell* pWellData) void nmDataAnalyzeManager::appendNmWellData(nmDataWellBase* pWellData) { + if(!ensurePebiResultOwnerThread("appendNmWellData")) { + return; + } if (pWellData == nullptr) return; bool bWellAdded = false; @@ -2441,7 +2456,7 @@ void nmDataAnalyzeManager::appendNmWellData(nmDataWellBase* pWellData) // 使旧网格失效。已由 createWell() 登记的井不重复增加输入版本号。 if(bWellAdded) { - m_oNumericalAnalysisCase.invalidateGrid(); + invalidatePebiGrid(); } // 通知面板新增井分组 @@ -2462,6 +2477,9 @@ void nmDataAnalyzeManager::appendNmWellData(nmDataWellBase* pWellData) bool nmDataAnalyzeManager::replaceWellData(nmDataWellBase* pOldWell, nmDataWellBase* pNewWell) { + if(!ensurePebiResultOwnerThread("replaceWellData")) { + return false; + } // 先完成所有无副作用的前置校验,失败时保持双方所有权不变。 if (pOldWell == nullptr || pNewWell == nullptr || pOldWell == pNewWell || m_vWellData.contains(pNewWell) || @@ -2495,7 +2513,7 @@ bool nmDataAnalyzeManager::replaceWellData(nmDataWellBase* pOldWell, } m_vWellData.insert(qMin(nOldIndex, m_vWellData.size()), pNewWell); // 井型改变会改变 PEBI 几何输入,旧求解器顺序和网格必须整体失效。 - m_oNumericalAnalysisCase.invalidateGrid(); + invalidatePebiGrid(); if (bWasCurrentWell) { m_pCurDataWell = pNewWell; @@ -3894,11 +3912,14 @@ NM_Grid_Type nmDataAnalyzeManager::getGridType() void nmDataAnalyzeManager::setGridType(NM_Grid_Type newGridType) { + if(!ensurePebiResultOwnerThread("setGridType")) { + return; + } if(m_eGridType == newGridType) { return; } m_eGridType = newGridType; - m_oNumericalAnalysisCase.invalidateGrid(); + invalidatePebiGrid(); } NM_SOLVER_MODEL_TYPE nmDataAnalyzeManager::getSolverModelType() const @@ -3908,11 +3929,14 @@ NM_SOLVER_MODEL_TYPE nmDataAnalyzeManager::getSolverModelType() const void nmDataAnalyzeManager::setSolverModelType(NM_SOLVER_MODEL_TYPE newSolverModelType) { + if(!ensurePebiResultOwnerThread("setSolverModelType")) { + return; + } if(m_eSolverModelType == newSolverModelType) { return; } m_eSolverModelType = newSolverModelType; - m_oNumericalAnalysisCase.invalidateResults(); + invalidatePebiResults(); } int nmDataAnalyzeManager::getPebiSolverType() const @@ -3922,11 +3946,14 @@ int nmDataAnalyzeManager::getPebiSolverType() const void nmDataAnalyzeManager::setPebiSolverType(int nSolverType) { + if(!ensurePebiResultOwnerThread("setPebiSolverType")) { + return; + } if((nSolverType == PebiSolverCpuAccelerated || nSolverType == PebiSolverOriginal) && m_nPebiSolverType != nSolverType) { m_nPebiSolverType = nSolverType; - m_oNumericalAnalysisCase.invalidateResults(); + invalidatePebiResults(); } } @@ -3937,11 +3964,14 @@ int nmDataAnalyzeManager::getPebiOmpThreads() const void nmDataAnalyzeManager::setPebiOmpThreads(int nOmpThreads) { + if(!ensurePebiResultOwnerThread("setPebiOmpThreads")) { + return; + } if(nOmpThreads == 1 || nOmpThreads == 2 || nOmpThreads == 4 || nOmpThreads == 8 || nOmpThreads == 16) { if(m_nPebiOmpThreads != nOmpThreads) { m_nPebiOmpThreads = nOmpThreads; - m_oNumericalAnalysisCase.invalidateResults(); + invalidatePebiResults(); } } } @@ -3953,10 +3983,13 @@ int nmDataAnalyzeManager::getPebiIluReuseSteps() const void nmDataAnalyzeManager::setPebiIluReuseSteps(int nIluReuseSteps) { + if(!ensurePebiResultOwnerThread("setPebiIluReuseSteps")) { + return; + } if(nIluReuseSteps >= 1 && nIluReuseSteps <= 100 && m_nPebiIluReuseSteps != nIluReuseSteps) { m_nPebiIluReuseSteps = nIluReuseSteps; - m_oNumericalAnalysisCase.invalidateResults(); + invalidatePebiResults(); } } @@ -4161,9 +4194,12 @@ nmDataLayer* nmDataAnalyzeManager::getLayerData() void nmDataAnalyzeManager::setLayers(QVector vecLayers) { + if(!ensurePebiResultOwnerThread("setLayers")) { + return; + } m_vecLayers = vecLayers; // 几何分层会改变储层离散输入,旧网格和旧结果不再有效。 - m_oNumericalAnalysisCase.invalidateGrid(); + invalidatePebiGrid(); emit dataChanged(); } @@ -4311,16 +4347,22 @@ nmDataAnalyzeManager::getNumericalAnalysisCase() const void nmDataAnalyzeManager::setPrimaryWellCode(const QString& sWellCode) { + if(!ensurePebiResultOwnerThread("setPrimaryWellCode")) { + return; + } if(!sWellCode.isEmpty() && findWellByCode(sWellCode) == nullptr) { return; } + const PebiResultStateSnapshot oPreviousResultState = + capturePebiResultState(); const quint64 nOldRevision = m_oNumericalAnalysisCase.getGridInputRevision(); m_oNumericalAnalysisCase.setPrimaryWellCode(sWellCode); if(m_oNumericalAnalysisCase.getGridInputRevision() != nOldRevision) { emit dataChanged(); } + notifyPebiResultStateChangedIfNeeded(oPreviousResultState); } QString nmDataAnalyzeManager::getPrimaryWellCode() const @@ -4330,12 +4372,18 @@ QString nmDataAnalyzeManager::getPrimaryWellCode() const void nmDataAnalyzeManager::setIncludeOtherWells(bool bInclude) { + if(!ensurePebiResultOwnerThread("setIncludeOtherWells")) { + return; + } + const PebiResultStateSnapshot oPreviousResultState = + capturePebiResultState(); const bool bOldInclude = m_oNumericalAnalysisCase.getIncludeOtherWells(); m_oNumericalAnalysisCase.setIncludeOtherWells(bInclude); if(bOldInclude != m_oNumericalAnalysisCase.getIncludeOtherWells()) { // 开关只改变当前求解输入。最后结果下拉框继续使用快照冻结的 UUID 列表。 emit dataChanged(); } + notifyPebiResultStateChangedIfNeeded(oPreviousResultState); } bool nmDataAnalyzeManager::getIncludeOtherWells() const @@ -4345,6 +4393,11 @@ bool nmDataAnalyzeManager::getIncludeOtherWells() const void nmDataAnalyzeManager::setPebiGridControl(double dGridControl) { + if(!ensurePebiResultOwnerThread("setPebiGridControl")) { + return; + } + const PebiResultStateSnapshot oPreviousResultState = + capturePebiResultState(); const quint64 nOldGridInputRevision = m_oNumericalAnalysisCase.getGridInputRevision(); m_oNumericalAnalysisCase.setPebiGridControl(dGridControl); @@ -4353,6 +4406,7 @@ void nmDataAnalyzeManager::setPebiGridControl(double dGridControl) // 分析方案已完成网格和结果失效,这里只通知已打开的网格窗口刷新。 emit dataChanged(); } + notifyPebiResultStateChangedIfNeeded(oPreviousResultState); } double nmDataAnalyzeManager::getPebiGridControl() const @@ -4363,6 +4417,11 @@ double nmDataAnalyzeManager::getPebiGridControl() const void nmDataAnalyzeManager::setIncludedCalculationWells( const QVector& vecWells) { + if(!ensurePebiResultOwnerThread("setIncludedCalculationWells")) { + return; + } + const PebiResultStateSnapshot oPreviousResultState = + capturePebiResultState(); const QVector vecOldWells = m_oNumericalAnalysisCase.getIncludedWells(); QVector vecValidWells; @@ -4399,6 +4458,7 @@ void nmDataAnalyzeManager::setIncludedCalculationWells( // 第三步:这里只通知当前输入变化,不修改独立快照中的结果井选择。 emit dataChanged(); } + notifyPebiResultStateChangedIfNeeded(oPreviousResultState); } QVector @@ -4610,12 +4670,24 @@ bool nmDataAnalyzeManager::markPebiGridBuiltIfCurrent( void nmDataAnalyzeManager::invalidatePebiGrid() { + if(!ensurePebiResultOwnerThread("invalidatePebiGrid")) { + return; + } + const PebiResultStateSnapshot oPreviousResultState = + capturePebiResultState(); m_oNumericalAnalysisCase.invalidateGrid(); + notifyPebiResultStateChangedIfNeeded(oPreviousResultState); } void nmDataAnalyzeManager::invalidatePebiResults() { + if(!ensurePebiResultOwnerThread("invalidatePebiResults")) { + return; + } + const PebiResultStateSnapshot oPreviousResultState = + capturePebiResultState(); m_oNumericalAnalysisCase.invalidateResults(); + notifyPebiResultStateChangedIfNeeded(oPreviousResultState); } bool nmDataAnalyzeManager::isPebiGridValid() const @@ -4649,6 +4721,59 @@ bool nmDataAnalyzeManager::isPebiResultSnapshotCurrent() const m_oNumericalAnalysisCase.getResultInputRevision(); } +nmDataAnalyzeManager::PebiResultStateSnapshot +nmDataAnalyzeManager::capturePebiResultState() const +{ + PebiResultStateSnapshot oState; + oState.m_bCurrent = false; + if(!ensurePebiResultOwnerThread("capturePebiResultState")) { + return oState; + } + oState.m_pSnapshot = m_pPebiResultSnapshot; + oState.m_sSnapshotId = m_pPebiResultSnapshot.isNull() + ? QString() : m_pPebiResultSnapshot->getSnapshotId(); + oState.m_bCurrent = isPebiResultSnapshotCurrent(); + return oState; +} + +void nmDataAnalyzeManager::notifyPebiResultStateChangedIfNeeded( + const PebiResultStateSnapshot& oPreviousState) +{ + if(!ensurePebiResultOwnerThread( + "notifyPebiResultStateChangedIfNeeded")) { + return; + } + const PebiResultStateSnapshot oCurrentState = + capturePebiResultState(); + const bool bSnapshotIdentityChanged = + oPreviousState.m_pSnapshot.data() != + oCurrentState.m_pSnapshot.data() || + oPreviousState.m_sSnapshotId != oCurrentState.m_sSnapshotId; + if(bSnapshotIdentityChanged || + oPreviousState.m_bCurrent != oCurrentState.m_bCurrent) { + emitPebiResultStateChangedOnOwnerThread(); + } +} + +bool nmDataAnalyzeManager::ensurePebiResultOwnerThread( + const char* pOperation) const +{ + const bool bOwnerThread = QThread::currentThread() == thread(); + Q_ASSERT_X(bOwnerThread, "nmDataAnalyzeManager", pOperation); + if(!bOwnerThread) { + // 结果快照引用和输入版本均未加锁;Release 也必须在首次读取前拒绝。 + qWarning() << "Rejected cross-thread PEBI result state mutation:" + << pOperation; + } + return bOwnerThread; +} + +void nmDataAnalyzeManager::emitPebiResultStateChangedOnOwnerThread() +{ + Q_ASSERT(QThread::currentThread() == thread()); + emit sigPebiResultStateChanged(); +} + QString nmDataAnalyzeManager::getCurrentResultWellInstanceId() const { return m_sCurrentResultWellInstanceId; @@ -4806,12 +4931,15 @@ bool nmDataAnalyzeManager::commitPebiResultSnapshot( return false; } + const PebiResultStateSnapshot oPreviousResultState = + capturePebiResultState(); // 发布点只替换只读引用;先前快照由仍在查看它的窗口延迟释放。 m_pPebiResultSnapshot = pPublishedCandidate; m_sCurrentResultWellInstanceId = sNextWellInstanceId; pCandidate.clear(); rebindPebiResultSnapshotToWells(); emit sigPebiResultSnapshotChanged(); + notifyPebiResultStateChangedIfNeeded(oPreviousResultState); return true; } @@ -4843,16 +4971,22 @@ void nmDataAnalyzeManager::rebindPebiResultSnapshotToWells() void nmDataAnalyzeManager::clearPebiResultSnapshot() { + if(!ensurePebiResultOwnerThread("clearPebiResultSnapshot")) { + return; + } if(m_pPebiResultSnapshot.isNull() && m_sCurrentResultWellInstanceId.isEmpty()) { return; } + const PebiResultStateSnapshot oPreviousResultState = + capturePebiResultState(); m_pPebiResultSnapshot.clear(); m_sCurrentResultWellInstanceId.clear(); rebindPebiResultSnapshotToWells(); emit sigPebiResultSnapshotChanged(); + notifyPebiResultStateChangedIfNeeded(oPreviousResultState); } // 设置当前查看井 @@ -4869,9 +5003,12 @@ nmDataWellBase* nmDataAnalyzeManager::getCurWellData() void nmDataAnalyzeManager::notifyDataChanged() { + if(!ensurePebiResultOwnerThread("notifyDataChanged")) { + return; + } // 本入口用于几何、井集合等网格输入;仅影响求解的参数应调用 // invalidatePebiResults(),避免无意义地重建当前有效网格。 - m_oNumericalAnalysisCase.invalidateGrid(); + invalidatePebiGrid(); emit dataChanged(); } @@ -4880,6 +5017,9 @@ void nmDataAnalyzeManager::onRegisteredAttributeEdited( QString sName, QVariant oValue) { + if(!ensurePebiResultOwnerThread("onRegisteredAttributeEdited")) { + return; + } Q_UNUSED(oValue); if(isPebiGridInputAttributeName(sName)) { @@ -4894,7 +5034,10 @@ void nmDataAnalyzeManager::onRegisteredAttributeEdited( void nmDataAnalyzeManager::notifyGeometryListChanged() { - m_oNumericalAnalysisCase.invalidateGrid(); + if(!ensurePebiResultOwnerThread("notifyGeometryListChanged")) { + return; + } + invalidatePebiGrid(); emit sigGeometryListChanged(); emit dataChanged(); } @@ -5206,9 +5349,13 @@ QVector nmDataAnalyzeManager::getPropertyInterpo void nmDataAnalyzeManager::setPropertyInterpolationDataSets( const QVector& dataSets) { + if(!ensurePebiResultOwnerThread( + "setPropertyInterpolationDataSets")) { + return; + } m_vecPropertyInterpolationDataSets = dataSets; // 属性插值只覆盖求解器单元属性,不改变二维 PEBI 拓扑。 - m_oNumericalAnalysisCase.invalidateResults(); + invalidatePebiResults(); } // 从 JSON 文件读取数据到 C++ 对象 @@ -6642,6 +6789,9 @@ bool nmDataAnalyzeManager::validateNmResultV3Window( bool nmDataAnalyzeManager::loadNmResult(QString sLoadAnalDir) { + if(!ensurePebiResultOwnerThread("loadNmResult")) { + return false; + } try { // v5 先在独立候选对象上完成全部解析和业务校验。 // 任一步失败时只析构候选,当前窗口已经发布的数据保持不变。 @@ -6660,10 +6810,13 @@ bool nmDataAnalyzeManager::loadNmResult(QString sLoadAnalDir) } } + const PebiResultStateSnapshot oPreviousResultState = + capturePebiResultState(); swapLoadedProjectState(oLoadedManager); rebuildLoadedAttributeRegistry(); rebindPebiResultSnapshotToWells(); emit sigPebiResultSnapshotChanged(); + notifyPebiResultStateChangedIfNeeded(oPreviousResultState); return true; } catch(const std::bad_alloc&) { diff --git a/Src/nmNum/nmSubWnd/nmSubWndMain.cpp b/Src/nmNum/nmSubWnd/nmSubWndMain.cpp index eff98bcb..17af29f3 100644 --- a/Src/nmNum/nmSubWnd/nmSubWndMain.cpp +++ b/Src/nmNum/nmSubWnd/nmSubWndMain.cpp @@ -37,6 +37,7 @@ #include "nmSubWndMain.h" #include "nmPlotDialogContext.h" #include "nmPlotDialogContextProvider.h" +#include "nmSubWndUtils.h" #include "nmWxGridVTKContainerWidget.h" #include "nmWxSelectWellsDlg.h" @@ -60,7 +61,6 @@ #include "nmReservoirPropertiesDataSource.h" #include "nmWxReservoirPropertiesEditor.h" #include "nmWxPropertyInterpolationDlg.h" -#include "nmWxPostprocessingAnimationWidget.h" #include "nmWxGeoRefDlg.h" #include "iSubWndFitting.h" @@ -2070,77 +2070,8 @@ bool nmSubWndMain::populateResultWidgets( iSubWndFitting* pSubWndFit, nmDataAnalyzeManager* pDataManager) { - if(pSubWndFit == nullptr || pDataManager == nullptr || - pDataManager->getGridType() != NM_Grid_PEBI || - nmDataAnalyzeManager::findManagerByFitting(pSubWndFit) != pDataManager) { - return false; - } - - QString sErrorMessage; - QWidget* p3DWidget = pSubWndFit->getFitSubRstWxOf( - FSRT_Nm3D, true, &sErrorMessage); - if(p3DWidget == nullptr) { - return false; - } - - nmWxPostprocessingAnimationWidget* pVtkWidget = - p3DWidget->findChild(); - if(pVtkWidget != nullptr) { - // 复用既有渲染窗口和 VTK 管道,只切换本轮已整体提交的数据。 - pVtkWidget->refreshResult(); - } else { - pVtkWidget = new nmWxPostprocessingAnimationWidget(NULL, pDataManager); - QLayout* p3DLayout = p3DWidget->layout(); - if(p3DLayout == nullptr) { - p3DLayout = new QVBoxLayout(p3DWidget); - } else { - QLayoutItem* pItem = nullptr; - while((pItem = p3DLayout->takeAt(0)) != nullptr) { - if(pItem->widget() != nullptr) { - delete pItem->widget(); - } - delete pItem; - } - } - p3DLayout->addWidget(pVtkWidget); - } - - // 结果参数界面 - QWidget* pResultPage = pSubWndFit->getFitSubRstWxOf( - FSRT_NmRst, true, &sErrorMessage); - if(pResultPage == nullptr) { - return false; - } - - nmWxParaPropertyPebi* pResultWidget = - pResultPage->findChild(); - if(pResultWidget == nullptr) { - // 与左侧共用属性面板类,结果区使用独立实例和只读参数列表。 - pResultWidget = new nmWxParaPropertyPebi; - pResultWidget->initUI(); - pResultWidget->setDataManager(pDataManager); - // 保持 8 月 24 日版本的稳定顺序:离屏完成参数构建后再加入可见布局。 - pResultWidget->rebuildResultParas(); - - QLayout* pResultLayout = pResultPage->layout(); - if(pResultLayout == nullptr) { - pResultLayout = new QVBoxLayout(pResultPage); - } else { - QLayoutItem* pItem = nullptr; - while((pItem = pResultLayout->takeAt(0)) != nullptr) { - if(pItem->widget() != nullptr) { - delete pItem->widget(); - } - delete pItem; - } - } - pResultLayout->addWidget(pResultWidget); - return true; - } - - pResultWidget->setDataManager(pDataManager); - pResultWidget->rebuildResultParas(); - return true; + return nmSubWndUtils::ensurePebiResultWidgets( + pSubWndFit, pDataManager); } void nmSubWndMain::schedulePendingResultWidgets() diff --git a/Src/nmNum/nmSubWnd/nmSubWndPostprocessing.cpp b/Src/nmNum/nmSubWnd/nmSubWndPostprocessing.cpp index 71c9860a..8ad8376c 100644 --- a/Src/nmNum/nmSubWnd/nmSubWndPostprocessing.cpp +++ b/Src/nmNum/nmSubWnd/nmSubWndPostprocessing.cpp @@ -32,19 +32,24 @@ #endif #include "nmDataDemo.h" +#include "nmDataAnalyzeManager.h" #include "nmDataLogFile.h" #include "nmGuiPlot.h" #include "nmSubWndPostprocessing.h" -#include "nmWxPostprocessingWidget.h" -#include "nmWxPostprocessingAnimationWidget.h" +#include "nmWxPebiResultViewWidget.h" #include #include -nmSubWndPostprocessing::nmSubWndPostprocessing(QWidget *parent, QString sExt) : - iSubWndBaseFit(parent, sExt) +nmSubWndPostprocessing::nmSubWndPostprocessing( + nmDataAnalyzeManager* pDataManager, + QWidget *parent, + QString sExt) : + iSubWndBaseFit(parent, sExt), + m_pDataManager(pDataManager), + m_pResultView(nullptr) { m_sMdiWndType = s_MdiType_DataAnal; setWindowTitle(tr("nmSubWndPostprocessing")); @@ -56,6 +61,21 @@ nmSubWndPostprocessing::~nmSubWndPostprocessing() #endif } +void nmSubWndPostprocessing::setDataManager( + nmDataAnalyzeManager* pDataManager) +{ + m_pDataManager = pDataManager; + if(!m_pResultView.isNull()) { + // 已有独立窗口复用同一个渲染基础设施,只切换其显式数据来源。 + m_pResultView->setDataManager(pDataManager); + } +} + +nmDataAnalyzeManager* nmSubWndPostprocessing::getDataManager() const +{ + return m_pDataManager.data(); +} + void nmSubWndPostprocessing::configWnd(QString sPrevInfo) { iSubWndBaseFit::configWnd(sPrevInfo); @@ -90,13 +110,10 @@ void nmSubWndPostprocessing::initUI() QWidget* nmSubWndPostprocessing::initUiMainWx() { - // 渲染 - //QString sDir = ZxBaseUtil::getCurWellDirOf("Nm/Solver"); - //QString sFile = sDir + "oilPost.vtk"; -// nmWxPostprocessingWidget* vtkWidget = new nmWxPostprocessingWidget(NULL); -// vtkWidget->loadVTKFile(sFile); - nmWxPostprocessingAnimationWidget* vtkWidget = new nmWxPostprocessingAnimationWidget(NULL); - return vtkWidget; + // 业务窗口只组合新结果视图,Manager 为空时由控件显示稳定空状态。 + m_pResultView = new nmWxPebiResultViewWidget( + m_pDataManager.data(), NULL); + return m_pResultView.data(); } bool nmSubWndPostprocessing::runCmdBy(QString sName, QString sID) diff --git a/Src/nmNum/nmSubWnd/nmSubWndUtils.cpp b/Src/nmNum/nmSubWnd/nmSubWndUtils.cpp index 3cd810da..2b05a8d0 100644 --- a/Src/nmNum/nmSubWnd/nmSubWndUtils.cpp +++ b/Src/nmNum/nmSubWnd/nmSubWndUtils.cpp @@ -3,9 +3,12 @@ #include #include #include +#include +#include #include #include #include +#include #include "zxSysUtils.h" #include "ZxUiBase.h" @@ -36,7 +39,7 @@ #include "nmWxGridDlg.h" -#include "nmWxPostprocessingAnimationWidget.h" +#include "nmWxPebiResultViewWidget.h" #include "nmGuiPlot.h" #include "ZxObjBase.h" @@ -162,6 +165,17 @@ iSubWnd* nmSubWndUtils::makesureSubWnd(QString sID, QString sExt, \ { bCreatedNewOne = false; + // 独立后处理窗口允许先以空 Manager 创建。再次点击入口时, + // 必须把当前分析显式转发给既有窗口,不能沿用旧绑定。 + if(_isSame(sID, "5116")) { + nmSubWndPostprocessing* pPostprocessing = + dynamic_cast(pSubWnd); + if(pPostprocessing != nullptr) { + pPostprocessing->setDataManager( + nmDataAnalyzeManager::getCurrentInstance()); + } + } + // TODO 根据需要,是否需要激活当前 if (i != pTabWx->currentIndex()) { @@ -235,7 +249,11 @@ iSubWnd* nmSubWndUtils::makesureSubWnd(QString sID, QString sExt, \ pSubWnd = pSubWndGrid; } else if(nID == 5116) { // 后处理 - nmSubWndPostprocessing* pSubWndPostProcessing = new nmSubWndPostprocessing(NULL, sExt); + nmDataAnalyzeManager* pDataManager = + nmDataAnalyzeManager::getCurrentInstance(); + // 没有当前分析时仍创建空窗口,后续可通过显式 setDataManager() 绑定。 + nmSubWndPostprocessing* pSubWndPostProcessing = + new nmSubWndPostprocessing(pDataManager, NULL, sExt); pSubWnd = pSubWndPostProcessing; } @@ -447,6 +465,86 @@ void nmSubWndUtils::setupNumericalDockWidgets(nmDataAnalyzeManager* pDataManager pWxDockNm2->setWidget(pParaWx); } +bool nmSubWndUtils::ensurePebiResultWidgets( + iSubWndFitting* pSubWndFit, + nmDataAnalyzeManager* pDataManager) +{ + if(pSubWndFit == nullptr || pDataManager == nullptr || + pDataManager->getGridType() != NM_Grid_PEBI || + nmDataAnalyzeManager::findManagerByFitting(pSubWndFit) != + pDataManager) { + return false; + } + + QString sErrorMessage; + QWidget* p3DPage = pSubWndFit->getFitSubRstWxOf( + FSRT_Nm3D, true, &sErrorMessage); + QWidget* pResultPage = pSubWndFit->getFitSubRstWxOf( + FSRT_NmRst, true, &sErrorMessage); + if(p3DPage == nullptr || pResultPage == nullptr) { + return false; + } + + nmWxPebiResultViewWidget* pResultView = + p3DPage->findChild(); + if(pResultView == nullptr) { + // 首次安装时复用载体已有布局,只移除旧业务控件和布局项。 + QLayout* p3DLayout = p3DPage->layout(); + if(p3DLayout == nullptr) { + QVBoxLayout* pNewLayout = new QVBoxLayout(p3DPage); + pNewLayout->setContentsMargins(0, 0, 0, 0); + pNewLayout->setSpacing(0); + p3DLayout = pNewLayout; + } else { + QLayoutItem* pItem = nullptr; + while((pItem = p3DLayout->takeAt(0)) != nullptr) { + if(pItem->widget() != nullptr) { + delete pItem->widget(); + } + delete pItem; + } + } + pResultView = new nmWxPebiResultViewWidget( + pDataManager, p3DPage); + p3DLayout->addWidget(pResultView); + } else { + // 相同 Manager 只会同步;不同 Manager 才由组合窗口完整清场再绑定。 + pResultView->setDataManager(pDataManager); + } + + nmWxParaPropertyPebi* pResultParameters = + pResultPage->findChild(); + if(pResultParameters == nullptr) { + QLayout* pResultLayout = pResultPage->layout(); + if(pResultLayout == nullptr) { + QVBoxLayout* pNewLayout = new QVBoxLayout(pResultPage); + pNewLayout->setContentsMargins(0, 0, 0, 0); + pNewLayout->setSpacing(0); + pResultLayout = pNewLayout; + } else { + QLayoutItem* pItem = nullptr; + while((pItem = pResultLayout->takeAt(0)) != nullptr) { + if(pItem->widget() != nullptr) { + delete pItem->widget(); + } + delete pItem; + } + } + // 保持既有稳定顺序:先在可见布局之外完成只读参数构建,再挂入页面。 + pResultParameters = new nmWxParaPropertyPebi(); + pResultParameters->initUI(); + pResultParameters->setDataManager(pDataManager); + pResultParameters->rebuildResultParas(); + pResultLayout->addWidget(pResultParameters); + return true; + } + + // 结果参数页没有监听快照状态信号,每次生产入口同步都必须显式重建。 + pResultParameters->setDataManager(pDataManager); + pResultParameters->rebuildResultParas(); + return true; +} + bool nmSubWndUtils::fillNmDockWxs(iSubWnd* pSubWnd) { iSubWndFitting* pSubWndF = dynamic_cast(pSubWnd); @@ -1062,113 +1160,19 @@ bool nmSubWndUtils::loadRsts(iSubWnd* pSubWnd, \ return false; } - if(pWxDockNm1->widget() != nullptr && - pWxDockNm2->widget() != nullptr) { - return true; // 与 develop 一致,已有数值面板不重复创建。 + if(pWxDockNm1->widget() == nullptr || + pWxDockNm2->widget() == nullptr) { + // 缺失左侧数值面板时先完成 Dock 装配,再统一维护两个结果页面。 + nmSubWndMain* pSubWndMain = new nmSubWndMain(NULL, ""); + ZxMainWindow* pMainWnd1 = const_cast(pMainWnd); + pSubWndMain->setMainWindow(pMainWnd1); + setupNumericalDockWidgets(pDataManager, pSubWndMain, + pWxDockNm1, pWxDockNm2); } - // 创建Map,暂时 - nmSubWndMain* pSubWndMain = new nmSubWndMain(NULL, ""); - ZxMainWindow* pMainWnd1 = const_cast(pMainWnd); - pSubWndMain->setMainWindow(pMainWnd1); - setupNumericalDockWidgets(pDataManager, pSubWndMain, - pWxDockNm1, pWxDockNm2); - - // v3 场图只以独立快照是否存在为准,不再依赖旧时间步 Map。 - if(pDataManager->hasPebiResultSnapshot()) { - QString sErrorMessage; - // 刷新场图 - QWidget* p3DWidget = pSubWndF->getFitSubRstWxOf(FSRT_Nm3D, true, &sErrorMessage); - - // 创建 vtkWidget - nmWxPostprocessingAnimationWidget* pVtkWidget = nullptr; - - pVtkWidget = new nmWxPostprocessingAnimationWidget( - nullptr, pDataManager); - - if(p3DWidget == nullptr || pVtkWidget == nullptr) { - delete pVtkWidget; - return false; - } - - // 先清理 - // 获取当前布局 - QLayout* p3DLayout = p3DWidget->layout(); - - if(p3DLayout) { - // 移除布局中的所有控件 - QLayoutItem* item; - - while((item = p3DLayout->takeAt(0))) { - if(item->widget()) { - // 删除控件 - delete item->widget(); - } - - delete item; // 删除布局项 - } - - // 删除布局 - delete p3DLayout; - } - - // 确保布局被移除 - p3DWidget->setLayout(nullptr); - - // 创建一个垂直布局管理器 - QVBoxLayout* p3DnewLayout = new QVBoxLayout(p3DWidget); - - // 将现有的控件添加到布局中 - p3DnewLayout->addWidget(pVtkWidget); - - // 设置 widget 的布局 - p3DWidget->setLayout(p3DnewLayout); - - // 刷新结果参数 - // 结果参数界面 - QWidget* pRstWidget = pSubWndF->getFitSubRstWxOf(FSRT_NmRst, true, &sErrorMessage); - if(pRstWidget == nullptr) { - return false; - } - - // 与左侧共用属性面板类,结果区使用独立实例和只读参数列表。 - nmWxParaPropertyPebi* pResultWidget = new nmWxParaPropertyPebi(); - pResultWidget->initUI(); - pResultWidget->setDataManager(pDataManager); - pResultWidget->rebuildResultParas(); - - // 先清理 - // 获取当前布局 - QLayout* pRstLayout = pRstWidget->layout(); - - if(pRstLayout) { - // 移除布局中的所有控件 - QLayoutItem* item; - - while((item = pRstLayout->takeAt(0))) { - if(item->widget()) { - // 删除控件 - delete item->widget(); - } - - delete item; // 删除布局项 - } - - // 删除布局 - delete pRstLayout; - } - - // 确保布局被移除 - pRstWidget->setLayout(nullptr); - - // 创建一个垂直布局管理器 - QVBoxLayout* pRstnewLayout = new QVBoxLayout(pRstWidget); - - // 将现有的控件添加到布局中 - pRstnewLayout->addWidget(pResultWidget); - - // 设置 widget 的布局 - pRstWidget->setLayout(pRstnewLayout); + // 即使两个 Dock 已存在,也必须刷新三维结果和只读结果参数页。 + if(!ensurePebiResultWidgets(pSubWndF, pDataManager)) { + return false; } } diff --git a/Src/nmNum/nmSubWxs/nmPebiResultPlaybackController.cpp b/Src/nmNum/nmSubWxs/nmPebiResultPlaybackController.cpp index 3b9b6dc8..427bbb35 100644 --- a/Src/nmNum/nmSubWxs/nmPebiResultPlaybackController.cpp +++ b/Src/nmNum/nmSubWxs/nmPebiResultPlaybackController.cpp @@ -43,6 +43,8 @@ nmPebiResultPlaybackController::nmPebiResultPlaybackController( : QObject(pParent), m_pSceneController(pSceneController), m_pTimer(new QTimer(this)), + m_nObservedBindingRevision(0), + m_nPlaybackBindingRevision(0), m_nLastPressureFrameIndex(0), m_nStepsPerSecond(DefaultPlaybackSpeed), m_bLoopEnabled(false), @@ -62,10 +64,11 @@ nmPebiResultPlaybackController::~nmPebiResultPlaybackController() void nmPebiResultPlaybackController::synchronizeFromScene() { - const QString sPreviousSnapshotId = m_sObservedSnapshotId; + const quint64 nPreviousBindingRevision = + m_nObservedBindingRevision; const bool bAvailable = synchronizeSceneIdentity(); const bool bSnapshotChanged = bAvailable && - sPreviousSnapshotId != m_sObservedSnapshotId; + nPreviousBindingRevision != m_nObservedBindingRevision; // 无快照或新快照必须终止旧回放;同快照同步只校正 Scene 已提交状态。 if(!bAvailable || bSnapshotChanged) { @@ -213,6 +216,16 @@ bool nmPebiResultPlaybackController::togglePlayback() } } + const quint64 nBindingRevision = + m_pSceneController->getSnapshotBindingRevision(); + if(nBindingRevision == 0 || + nBindingRevision != m_nObservedBindingRevision) { + return finishFailedOperation(playbackText( + "Result playback is unavailable.")); + } + + // Timer 归属只在全部校验和末帧复位成功后发布,防止旧回调推进新绑定。 + m_nPlaybackBindingRevision = nBindingRevision; m_pTimer->setInterval(calculatePlaybackInterval()); m_pTimer->start(); m_bPlaying = true; @@ -263,6 +276,11 @@ QString nmPebiResultPlaybackController::getObservedSnapshotId() const return m_sObservedSnapshotId; } +quint64 nmPebiResultPlaybackController::getObservedBindingRevision() const +{ + return m_nObservedBindingRevision; +} + QStringList nmPebiResultPlaybackController::getAvailablePropertyIds() const { return isAvailable() @@ -342,14 +360,23 @@ bool nmPebiResultPlaybackController::isPlaying() const void nmPebiResultPlaybackController::onPlaybackTimeout() { - const QString sPreviousSnapshotId = m_sObservedSnapshotId; - if(!synchronizeSceneIdentity()) { + // 先验证 Timer 启动时捕获的绑定,已经排队的旧事件不得推进后来绑定的快照。 + const bool bTimerOwnsCurrentBinding = + m_bPlaying && m_pTimer != nullptr && m_pTimer->isActive() && + m_pSceneController != nullptr && + m_pSceneController->hasSnapshot() && + m_nPlaybackBindingRevision != 0 && + m_pSceneController->getSnapshotBindingRevision() == + m_nPlaybackBindingRevision; + if(!bTimerOwnsCurrentBinding) { stopTimerWithoutSignal(); emit stateChanged(); return; } - if(sPreviousSnapshotId != m_sObservedSnapshotId) { - // 旧 Timer 已排队的回调不得推进刚提交的新快照。 + + // Timer 归属通过后再做普通身份同步;任何不一致都只停止,不提交下一帧。 + if(!synchronizeSceneIdentity() || !m_bPlaying || + m_nPlaybackBindingRevision == 0) { stopTimerWithoutSignal(); emit stateChanged(); return; @@ -400,6 +427,7 @@ bool nmPebiResultPlaybackController::stopTimerWithoutSignal() m_pTimer->stop(); } m_bPlaying = false; + m_nPlaybackBindingRevision = 0; return bWasPlaying; } @@ -409,22 +437,28 @@ bool nmPebiResultPlaybackController::synchronizeSceneIdentity() !m_pSceneController->hasSnapshot()) { stopTimerWithoutSignal(); m_sObservedSnapshotId.clear(); + m_nObservedBindingRevision = 0; m_nLastPressureFrameIndex = 0; return false; } const QString sSnapshotId = m_pSceneController->getCurrentSnapshotId(); - if(sSnapshotId.isEmpty()) { + const quint64 nBindingRevision = + m_pSceneController->getSnapshotBindingRevision(); + if(sSnapshotId.isEmpty() || nBindingRevision == 0) { stopTimerWithoutSignal(); m_sObservedSnapshotId.clear(); + m_nObservedBindingRevision = 0; m_nLastPressureFrameIndex = 0; return false; } - if(m_sObservedSnapshotId != sSnapshotId) { + if(m_nObservedBindingRevision != nBindingRevision || + m_sObservedSnapshotId != sSnapshotId) { stopTimerWithoutSignal(); m_sObservedSnapshotId = sSnapshotId; + m_nObservedBindingRevision = nBindingRevision; // 新快照由 SceneController 固定提交压力第 0 帧;无压力分支仍从 0 开始。 m_nLastPressureFrameIndex = isPressureProperty(getCurrentPropertyId()) && diff --git a/Src/nmNum/nmSubWxs/nmPebiResultSceneController.cpp b/Src/nmNum/nmSubWxs/nmPebiResultSceneController.cpp index fffe4004..14d84c70 100644 --- a/Src/nmNum/nmSubWxs/nmPebiResultSceneController.cpp +++ b/Src/nmNum/nmSubWxs/nmPebiResultSceneController.cpp @@ -398,6 +398,7 @@ public: m_pWellLayer(nullptr), m_pAdapter(nullptr), m_nCurrentFrameIndex(-1), + m_nSnapshotBindingRevision(0), m_bHasPropertyRange(false), m_bHasBounds(false), m_bMeshVisible(true), @@ -874,6 +875,8 @@ public: /** @brief 按稳定属性 ID 保存的窗口级样式;clear 不删除。 */ QMap m_mapDisplaySettings; int m_nCurrentFrameIndex; + /** @brief 不同快照对象完整提交成功后的单调序号,clear 和失败均不回退。 */ + quint64 m_nSnapshotBindingRevision; double m_dBusinessRange[2]; double m_dDisplayRange[2]; double m_dBounds[6]; @@ -1123,6 +1126,8 @@ bool nmPebiResultSceneController::setSnapshot( Q_UNUSED(bCenterReset); // showReadyState 是本提交路径唯一的 Render 入口。 m_pPrivate->m_pContainer->showReadyState(); + // 候选管线、相机和首次 Render 已完整提交,最后才发布新绑定序号。 + ++m_pPrivate->m_nSnapshotBindingRevision; return true; } @@ -1239,6 +1244,7 @@ bool nmPebiResultSceneController::applyPropertyFrame( } bool nmPebiResultSceneController::applyCurrentDisplaySettings( + quint64 nExpectedBindingRevision, const QString& sExpectedSnapshotId, const QString& sExpectedPropertyId, const nmPebiResultDisplaySettings& oSettings, @@ -1254,6 +1260,8 @@ bool nmPebiResultSceneController::applyCurrentDisplaySettings( // 身份核对和实际提交处在同一个 GUI 线程调用中,旧对话框不能污染新快照。 if(!isResultSceneGuiThread() || !hasSnapshot() || + nExpectedBindingRevision == 0 || + nExpectedBindingRevision != getSnapshotBindingRevision() || sExpectedSnapshotId.isEmpty() || sExpectedPropertyId.isEmpty() || sExpectedSnapshotId != getCurrentSnapshotId() || @@ -1427,6 +1435,12 @@ bool nmPebiResultSceneController::hasSnapshot() const m_pPrivate->m_pWellLayer->hasActiveAnnotations(); } +quint64 nmPebiResultSceneController::getSnapshotBindingRevision() const +{ + return m_pPrivate != nullptr + ? m_pPrivate->m_nSnapshotBindingRevision : 0; +} + QString nmPebiResultSceneController::getCurrentSnapshotId() const { return hasSnapshot() diff --git a/Src/nmNum/nmSubWxs/nmWxPebiResultColorScaleSettingsDlg.cpp b/Src/nmNum/nmSubWxs/nmWxPebiResultColorScaleSettingsDlg.cpp index c881d570..c23bf501 100644 --- a/Src/nmNum/nmSubWxs/nmWxPebiResultColorScaleSettingsDlg.cpp +++ b/Src/nmNum/nmSubWxs/nmWxPebiResultColorScaleSettingsDlg.cpp @@ -79,6 +79,7 @@ nmWxPebiResultColorScaleSettingsDlg( QWidget* pParent) : QDialog(pParent), m_pSceneController(pSceneController), + m_nExpectedBindingRevision(0), m_bManualRangeEdited(false), m_bInitialized(false), m_pColorMapCombo(nullptr), @@ -324,6 +325,8 @@ bool nmWxPebiResultColorScaleSettingsDlg::initializeFromScene() if(m_pSceneController == nullptr || !m_pSceneController->hasSnapshot()) { return false; } + m_nExpectedBindingRevision = + m_pSceneController->getSnapshotBindingRevision(); m_sExpectedSnapshotId = m_pSceneController->getCurrentSnapshotId(); m_sExpectedPropertyId = m_pSceneController->getCurrentPropertyId(); m_sDisplayUnit = nmPebiResultPropertyDisplayText::getDefaultDisplayUnit( @@ -331,7 +334,8 @@ bool nmWxPebiResultColorScaleSettingsDlg::initializeFromScene() m_pUnitLabel->setText(m_sDisplayUnit.isEmpty() ? tr("Dimensionless") : m_sDisplayUnit); double dVisibleRange[2] = { 0.0, 0.0 }; - if(m_sExpectedSnapshotId.isEmpty() || m_sExpectedPropertyId.isEmpty() || + if(m_nExpectedBindingRevision == 0 || + m_sExpectedSnapshotId.isEmpty() || m_sExpectedPropertyId.isEmpty() || !m_pSceneController->getCurrentPropertyRange(m_dPropertyRange) || !m_pSceneController->getCurrentDisplayRange(dVisibleRange) || !m_pSceneController->getCurrentDisplaySettings( @@ -421,6 +425,7 @@ bool nmWxPebiResultColorScaleSettingsDlg::applyEditor() QString sError; if(!collectEditedSettings(oSettings, sError) || !m_pSceneController->applyCurrentDisplaySettings( + m_nExpectedBindingRevision, m_sExpectedSnapshotId, m_sExpectedPropertyId, oSettings, &sError)) { setErrorText(sError); diff --git a/Src/nmNum/nmSubWxs/nmWxPebiResultControlBar.cpp b/Src/nmNum/nmSubWxs/nmWxPebiResultControlBar.cpp index 1eefd9cc..c4888478 100644 --- a/Src/nmNum/nmSubWxs/nmWxPebiResultControlBar.cpp +++ b/Src/nmNum/nmSubWxs/nmWxPebiResultControlBar.cpp @@ -55,6 +55,7 @@ nmWxPebiResultControlBar::nmWxPebiResultControlBar( m_pLastButton(nullptr), m_pPlayPauseButton(nullptr), m_pSettingsButton(nullptr), + m_nDisplayedBindingRevision(0), m_nDisplayedTimeFrameCount(-1) { Q_ASSERT(pSceneController != nullptr); @@ -102,8 +103,10 @@ void nmWxPebiResultControlBar::onPropertyIndexChanged(int nIndex) if(m_pPlaybackController == nullptr || nIndex < 0) { return; } - m_pPlaybackController->selectProperty( - m_pPropertyCombo->itemData(nIndex).toString()); + if(m_pPlaybackController->selectProperty( + m_pPropertyCombo->itemData(nIndex).toString())) { + emit operationSucceeded(); + } } void nmWxPebiResultControlBar::onTimeIndexChanged(int nIndex) @@ -111,8 +114,10 @@ void nmWxPebiResultControlBar::onTimeIndexChanged(int nIndex) if(m_pPlaybackController == nullptr || nIndex < 0) { return; } - m_pPlaybackController->selectFrame( - m_pTimeCombo->itemData(nIndex).toInt()); + if(m_pPlaybackController->selectFrame( + m_pTimeCombo->itemData(nIndex).toInt())) { + emit operationSucceeded(); + } } void nmWxPebiResultControlBar::onColorSettingsClicked() @@ -128,6 +133,8 @@ void nmWxPebiResultControlBar::onColorSettingsClicked() m_pSceneController, this); connect(&oDialog, SIGNAL(displaySettingsApplied()), this, SLOT(refreshColorPreview())); + connect(&oDialog, SIGNAL(displaySettingsApplied()), + this, SIGNAL(operationSucceeded())); connect(&oDialog, SIGNAL(operationFailed(QString)), this, SLOT(forwardOperationFailure(QString))); oDialog.exec(); @@ -136,27 +143,37 @@ void nmWxPebiResultControlBar::onColorSettingsClicked() void nmWxPebiResultControlBar::onFirstFrameClicked() { - m_pPlaybackController->selectFirstFrame(); + if(m_pPlaybackController->selectFirstFrame()) { + emit operationSucceeded(); + } } void nmWxPebiResultControlBar::onPreviousFrameClicked() { - m_pPlaybackController->selectPreviousFrame(); + if(m_pPlaybackController->selectPreviousFrame()) { + emit operationSucceeded(); + } } void nmWxPebiResultControlBar::onNextFrameClicked() { - m_pPlaybackController->selectNextFrame(); + if(m_pPlaybackController->selectNextFrame()) { + emit operationSucceeded(); + } } void nmWxPebiResultControlBar::onLastFrameClicked() { - m_pPlaybackController->selectLastFrame(); + if(m_pPlaybackController->selectLastFrame()) { + emit operationSucceeded(); + } } void nmWxPebiResultControlBar::onPlayPauseClicked() { - m_pPlaybackController->togglePlayback(); + if(m_pPlaybackController->togglePlayback()) { + emit operationSucceeded(); + } } void nmWxPebiResultControlBar::onAnimationSettingsClicked() @@ -167,9 +184,11 @@ void nmWxPebiResultControlBar::onAnimationSettingsClicked() m_pPlaybackController->getPlaybackSpeed(), m_pPlaybackController->isLoopEnabled(), this); if(oDialog.exec() == QDialog::Accepted) { - m_pPlaybackController->setPlaybackSettings( - oDialog.getPlaybackSpeed(), - oDialog.isLoopEnabled()); + if(m_pPlaybackController->setPlaybackSettings( + oDialog.getPlaybackSpeed(), + oDialog.isLoopEnabled())) { + emit operationSucceeded(); + } } } @@ -184,6 +203,7 @@ void nmWxPebiResultControlBar::refreshFromController() m_pPropertyCombo->blockSignals(bPropertyBlocked); clearTimeItems(); m_sDisplayedSnapshotId.clear(); + m_nDisplayedBindingRevision = 0; m_listDisplayedPropertyIds.clear(); m_pPropertyCombo->setEnabled(false); m_pColorSettingsButton->setEnabled(false); @@ -202,14 +222,18 @@ void nmWxPebiResultControlBar::refreshFromController() const QString sSnapshotId = m_pPlaybackController->getObservedSnapshotId(); + const quint64 nBindingRevision = + m_pPlaybackController->getObservedBindingRevision(); const QStringList listPropertyIds = m_pPlaybackController->getAvailablePropertyIds(); const bool bSnapshotChanged = + m_nDisplayedBindingRevision != nBindingRevision || m_sDisplayedSnapshotId != sSnapshotId; if(bSnapshotChanged || m_listDisplayedPropertyIds != listPropertyIds) { rebuildPropertyItems(listPropertyIds); m_sDisplayedSnapshotId = sSnapshotId; + m_nDisplayedBindingRevision = nBindingRevision; m_listDisplayedPropertyIds = listPropertyIds; // 即使属性和帧数相同,新快照的真实时间值也可能不同,不能复用旧列表。 diff --git a/Src/nmNum/nmSubWxs/nmWxPebiResultViewWidget.cpp b/Src/nmNum/nmSubWxs/nmWxPebiResultViewWidget.cpp new file mode 100644 index 00000000..59cbff22 --- /dev/null +++ b/Src/nmNum/nmSubWxs/nmWxPebiResultViewWidget.cpp @@ -0,0 +1,577 @@ +#include "nmWxPebiResultViewWidget.h" + +#include "nmDataAnalyzeManager.h" +#include "nmPebiResultSceneController.h" +#include "nmPebiResultSnapshot.h" +#include "nmVTKViewCommandController.h" +#include "nmWxPebiResultControlBar.h" +#include "nmWxVTKRenderContainerWidget.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +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); +} + +} + +nmWxPebiResultViewWidget::nmWxPebiResultViewWidget( + nmDataAnalyzeManager* pDataManager, + QWidget* pParent) + : QWidget(pParent), + m_pRenderContainer(nullptr), + m_pSceneController(nullptr), + m_pControlBar(nullptr), + m_pToolBar(nullptr), + m_pStatusLabel(nullptr), + m_pMeshAction(nullptr), + m_pScalarBarAction(nullptr), + m_pScaleGridAction(nullptr), + m_pCellEdgesAction(nullptr), + m_pWellMarkersAction(nullptr), + m_pWellNamesAction(nullptr), + m_bObservedSnapshotCurrent(false), + m_bObservedStateValid(false), + m_bSynchronizing(false), + m_bSyncPending(false) +{ + createUi(); + setDataManager(pDataManager); +} + +nmWxPebiResultViewWidget::~nmWxPebiResultViewWidget() +{ + disconnectDataManager(); + 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; + } + + disconnectDataManager(); + 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(m_bSynchronizing) { + // 极端嵌套事件下不得在快照提交栈内递归清场,退出当前事务后再读空状态。 + m_bSyncPending = true; + return; + } + clearSceneAndControls(tr("No result data.")); + // 记录稳定空状态,后续重复同步空 Manager 时不再清场或切换状态页。 + rememberObservedState( + QSharedPointer(), + QString(), false); +} + +void nmWxPebiResultViewWidget::appendContextMenuActions(QMenu* pMenu) +{ + if(pMenu == nullptr) { + return; + } + pMenu->addAction(m_pMeshAction); + pMenu->addAction(m_pScalarBarAction); + pMenu->addAction(m_pScaleGridAction); + pMenu->addAction(m_pCellEdgesAction); + pMenu->addAction(m_pWellMarkersAction); + pMenu->addAction(m_pWellNamesAction); +} + +void nmWxPebiResultViewWidget::onControlOperationFailed( + const QString& sError) +{ + // 高优先级状态存在时丢弃普通错误,避免其在过期提示解除后突然出现。 + if(!m_sBindingError.isEmpty() || + (m_bObservedStateValid && !m_bObservedSnapshotCurrent)) { + return; + } + m_sOperationError = sError; + refreshStatusBar(); +} + +void nmWxPebiResultViewWidget::onControlOperationSucceeded() +{ + if(m_sOperationError.isEmpty()) { + return; + } + // 这里只清除最低优先级的普通操作错误;刷新函数仍优先显示绑定失败和过期。 + m_sOperationError.clear(); + refreshStatusBar(); +} + +void nmWxPebiResultViewWidget::onMeshToggled(bool bVisible) +{ + if(m_pSceneController != nullptr && m_pSceneController->hasSnapshot()) { + m_pSceneController->setMeshVisible(bVisible); + } +} + +void nmWxPebiResultViewWidget::onScalarBarToggled(bool bVisible) +{ + if(m_pSceneController != nullptr && m_pSceneController->hasSnapshot()) { + m_pSceneController->setScalarBarVisible(bVisible); + } +} + +void nmWxPebiResultViewWidget::onScaleGridToggled(bool bVisible) +{ + if(m_pSceneController != nullptr && m_pSceneController->hasSnapshot()) { + m_pSceneController->setScaleGridVisible(bVisible); + } +} + +void nmWxPebiResultViewWidget::onCellEdgesToggled(bool bVisible) +{ + if(m_pSceneController != nullptr && m_pSceneController->hasSnapshot()) { + m_pSceneController->setCellEdgesVisible(bVisible); + } +} + +void nmWxPebiResultViewWidget::onWellMarkersToggled(bool bVisible) +{ + if(m_pSceneController != nullptr && m_pSceneController->hasSnapshot()) { + m_pSceneController->setWellMarkersVisible(bVisible); + } +} + +void nmWxPebiResultViewWidget::onWellNamesToggled(bool bVisible) +{ + 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_SaveImage | + nmVTKViewCommandController::ViewCommand_CopyImage | + nmVTKViewCommandController::ViewCommand_Print | + nmVTKViewCommandController::ViewCommand_PrintPreview); + m_pSceneController = new nmPebiResultSceneController(m_pRenderContainer); + + 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_pMeshAction = new QAction(QIcon(resultViewIconPath("NmGrid.png")), + tr("Result Grid"), this); + 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_pCellEdgesAction = new QAction( + QIcon(resultViewIconPath("SpecialLineVisible.png")), + tr("Cell Edges"), this); + m_pWellMarkersAction = new QAction( + QIcon(resultViewIconPath("NmWell.png")), + tr("Well Markers"), this); + m_pWellNamesAction = new QAction( + QIcon(resultViewIconPath("NmShow.png")), + tr("Well Names"), this); + + QAction* const pBusinessActions[] = { + m_pMeshAction, m_pScalarBarAction, m_pScaleGridAction, + m_pCellEdgesAction, m_pWellMarkersAction, m_pWellNamesAction + }; + for(int nIndex = 0; nIndex < 6; ++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(); + for(int nIndex = 0; nIndex < 6; ++nIndex) { + m_pToolBar->addAction(pBusinessActions[nIndex]); + } + m_pToolBar->addSeparator(); + if(pCopyAction != nullptr) m_pToolBar->addAction(pCopyAction); + } + + connect(m_pMeshAction, SIGNAL(toggled(bool)), + this, SLOT(onMeshToggled(bool))); + connect(m_pScalarBarAction, SIGNAL(toggled(bool)), + this, SLOT(onScalarBarToggled(bool))); + connect(m_pScaleGridAction, SIGNAL(toggled(bool)), + this, SLOT(onScaleGridToggled(bool))); + connect(m_pCellEdgesAction, SIGNAL(toggled(bool)), + this, SLOT(onCellEdgesToggled(bool))); + connect(m_pWellMarkersAction, SIGNAL(toggled(bool)), + this, SLOT(onWellMarkersToggled(bool))); + connect(m_pWellNamesAction, SIGNAL(toggled(bool)), + this, SLOT(onWellNamesToggled(bool))); + 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())); + pMainLayout->addWidget(m_pControlBar); + + synchronizeBusinessActions(); + m_pRenderContainer->showEmptyState(tr("No result data.")); +} + +void nmWxPebiResultViewWidget::synchronizeOnce() +{ + if(m_pDataManager.isNull()) { + if(m_bObservedStateValid && m_pObservedSnapshot.isNull()) { + return; + } + clearSceneAndControls(tr("No result data.")); + rememberObservedState( + QSharedPointer(), + QString(), false); + return; + } + + const QSharedPointer pSnapshot = + m_pDataManager->getPebiResultSnapshot(); + const QString sSnapshotId = pSnapshot.isNull() + ? QString() : pSnapshot->getSnapshotId(); + const bool bCurrent = !pSnapshot.isNull() && + m_pDataManager->isPebiResultSnapshotCurrent(); + const bool bSameIdentity = m_bObservedStateValid && + m_pObservedSnapshot.data() == pSnapshot.data() && + m_sObservedSnapshotId == sSnapshotId; + const bool bSameState = bSameIdentity && + m_bObservedSnapshotCurrent == bCurrent; + if(bSameState) { + return; + } + + const bool bBecameStale = bSameIdentity && + m_bObservedSnapshotCurrent && !bCurrent; + m_sOperationError.clear(); + + if(pSnapshot.isNull()) { + clearSceneAndControls(tr("No result data.")); + rememberObservedState(pSnapshot, sSnapshotId, false); + return; + } + + if(!bSameIdentity) { + // 候选提交前停止本窗口 Timer;失败时旧 Scene 和最后压力帧记忆仍保留。 + m_pControlBar->stopPlayback(); + QString sError; + const bool bBound = m_pSceneController->setSnapshot( + pSnapshot, &sError); + rememberObservedState(pSnapshot, sSnapshotId, bCurrent); + if(bBound) { + m_pDisplayedSnapshot = pSnapshot; + m_sDisplayedSnapshotId = sSnapshotId; + m_sBindingError.clear(); + // 新成功绑定序号会让 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(); + } + 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); + } +} + +void nmWxPebiResultViewWidget::rememberObservedState( + const QSharedPointer& pSnapshot, + const QString& sSnapshotId, + bool bCurrent) +{ + m_pObservedSnapshot = pSnapshot; + m_sObservedSnapshotId = sSnapshotId; + m_bObservedSnapshotCurrent = bCurrent; + m_bObservedStateValid = true; +} + +void nmWxPebiResultViewWidget::synchronizeBusinessActions() +{ + if(m_pSceneController == nullptr) { + return; + } + setActionCheckedWithoutSignal( + m_pMeshAction, m_pSceneController->isMeshVisible()); + setActionCheckedWithoutSignal( + m_pScalarBarAction, m_pSceneController->isScalarBarVisible()); + setActionCheckedWithoutSignal( + m_pScaleGridAction, m_pSceneController->isScaleGridVisible()); + setActionCheckedWithoutSignal( + m_pCellEdgesAction, m_pSceneController->areCellEdgesVisible()); + setActionCheckedWithoutSignal( + m_pWellMarkersAction, m_pSceneController->areWellMarkersVisible()); + setActionCheckedWithoutSignal( + m_pWellNamesAction, m_pSceneController->areWellNamesVisible()); + + const bool bEnabled = m_pSceneController->hasSnapshot(); + m_pMeshAction->setEnabled(bEnabled); + m_pScalarBarAction->setEnabled(bEnabled); + m_pScaleGridAction->setEnabled(bEnabled); + m_pCellEdgesAction->setEnabled(bEnabled); + m_pWellMarkersAction->setEnabled(bEnabled); + m_pWellNamesAction->setEnabled(bEnabled); +} + +void nmWxPebiResultViewWidget::refreshStatusBar() +{ + 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_bObservedStateValid && !m_bObservedSnapshotCurrent) { + m_pStatusLabel->setStyleSheet( + "QLabel { color: #6b5200; background: #fff4bf; } "); + m_pStatusLabel->setText(tr( + "Current inputs have changed. Showing the last successful result.")); + 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; +}