From d62a7241cda36f5bea437f88abc6870757dde1e3 Mon Sep 17 00:00:00 2001 From: lh <2334563547@qq.com> Date: Fri, 18 Sep 2026 09:17:58 +0800 Subject: [PATCH] =?UTF-8?q?feat(nmNum):=20=E6=8E=A5=E5=85=A5=E7=8B=AC?= =?UTF-8?q?=E7=AB=8BPEBI=E7=BB=93=E6=9E=9C=E8=A7=86=E5=9B=BE=E4=B8=8E?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增结果组合窗口,整合渲染场景、播放控制条、显示工具栏和状态提示 - 统一计算完成、成果加载和独立后处理窗口的结果视图安装流程 - 增加结果快照身份及新鲜度通知,输入变化后保留最后成功画面 - 使用绑定序号隔离同ID快照、播放定时器和颜色设置事务 - 支持独立后处理窗口在复用时重新绑定当前DataManager - 成功操作后清除历史普通错误,并保留过期及绑定错误优先级 - 限制PEBI结果状态修改在Manager所属线程执行 - 移除生产入口对旧动画结果控件的依赖 - 更新中文翻译 --- Bin/Config/Lang/cn/nmNum_cn.qm | Bin 146947 -> 147835 bytes Bin/Config/Lang/cn/nmNum_cn.ts | 13 + Include/nmNum/nmData/nmDataAnalyzeManager.h | 32 +- .../nmNum/nmSubWnd/nmSubWndPostprocessing.h | 21 +- Include/nmNum/nmSubWnd/nmSubWndUtils.h | 8 + .../nmSubWxs/nmPebiResultPlaybackController.h | 9 +- .../nmSubWxs/nmPebiResultSceneController.h | 8 + .../nmWxPebiResultColorScaleSettingsDlg.h | 7 +- .../nmNum/nmSubWxs/nmWxPebiResultControlBar.h | 5 + .../nmNum/nmSubWxs/nmWxPebiResultViewWidget.h | 134 ++++ Src/nmNum/nmData/nmDataAnalyzeManager.cpp | 179 +++++- Src/nmNum/nmSubWnd/nmSubWndMain.cpp | 75 +-- Src/nmNum/nmSubWnd/nmSubWndPostprocessing.cpp | 39 +- Src/nmNum/nmSubWnd/nmSubWndUtils.cpp | 218 +++---- .../nmPebiResultPlaybackController.cpp | 50 +- .../nmSubWxs/nmPebiResultSceneController.cpp | 14 + .../nmWxPebiResultColorScaleSettingsDlg.cpp | 7 +- .../nmSubWxs/nmWxPebiResultControlBar.cpp | 48 +- .../nmSubWxs/nmWxPebiResultViewWidget.cpp | 577 ++++++++++++++++++ 19 files changed, 1214 insertions(+), 230 deletions(-) create mode 100644 Include/nmNum/nmSubWxs/nmWxPebiResultViewWidget.h create mode 100644 Src/nmNum/nmSubWxs/nmWxPebiResultViewWidget.cpp diff --git a/Bin/Config/Lang/cn/nmNum_cn.qm b/Bin/Config/Lang/cn/nmNum_cn.qm index 461b8f773ad198c92a94b70202bb2b3214b3fdd6..e7a2d926c1f2ca5a5f4fccba675ccf21fcd8c3ba 100644 GIT binary patch delta 4647 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 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; +}