diff --git a/Bin/Config/Lang/cn/nmNum_cn.qm b/Bin/Config/Lang/cn/nmNum_cn.qm index ee4d0fb1..eee352b4 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 0a82dccf..d9e76056 100644 --- a/Bin/Config/Lang/cn/nmNum_cn.ts +++ b/Bin/Config/Lang/cn/nmNum_cn.ts @@ -2803,6 +2803,18 @@ Reason: %1 Unable to create the grid render layers.无法创建网格渲染图层。 Unable to bind the grid render data.无法绑定网格渲染数据。 + + nmPebiResultSceneController + The result snapshot is invalid.结果快照无效。 + The result snapshot has no renderable property.结果快照中没有可渲染属性。 + Unable to initialize the result render adapter.无法初始化结果渲染适配器。 + The result render data failed validation.结果渲染数据校验失败。 + Unable to create the result render layers.无法创建结果渲染图层。 + Unable to commit the result render data.无法提交结果渲染数据。 + The requested result property or frame is invalid.请求的结果属性或帧无效。 + The result property range is invalid.结果属性范围无效。 + Unable to apply the result property or frame.无法应用结果属性或帧。 + nmWxPebiGridViewWidget No grid data.暂无网格数据。 diff --git a/Include/nmNum/nmData/nmPebiResultSnapshotRenderAdapter.h b/Include/nmNum/nmData/nmPebiResultSnapshotRenderAdapter.h index 3962c23b..19595ed2 100644 --- a/Include/nmNum/nmData/nmPebiResultSnapshotRenderAdapter.h +++ b/Include/nmNum/nmData/nmPebiResultSnapshotRenderAdapter.h @@ -67,6 +67,8 @@ public: int getCurrentFrameIndex() const; /** @brief 返回当前已经绑定的属性 ID,尚未绑定时为空。 */ QString getCurrentPropertyId() const; + /** @brief 返回当前属性对应的稳定 VTK 数组名,尚未绑定时为空。 */ + QString getCurrentScalarArrayName() const; /** @brief 返回适配器渲染网格的单元数。 */ vtkIdType getCellCount() const; /** @brief 返回当前是否使用零整帧复制的共享压力模式。 */ diff --git a/Include/nmNum/nmRender/nmVTKCameraController.h b/Include/nmNum/nmRender/nmVTKCameraController.h index 9b5faf29..d65d33a5 100644 --- a/Include/nmNum/nmRender/nmVTKCameraController.h +++ b/Include/nmNum/nmRender/nmVTKCameraController.h @@ -24,13 +24,18 @@ public: /** @brief 返回当前 Renderer 非所有权指针。 */ vtkRenderer* getRenderer() const; - /** @brief 恢复透视投影并适配当前可见边界。 */ + /** @brief 复制并启用主内容边界;参数有限且各轴最小值不大于最大值。 */ + bool setPrimaryViewBounds(const double dBounds[6]); + /** @brief 清除主内容边界,使相机操作重新使用全部可见 Prop。 */ + void clearPrimaryViewBounds(); + + /** @brief 恢复透视投影并适配主内容或当前可见边界。 */ bool resetCamera(); /** @brief 按固定倍率 1.2 放大当前视图,保持观察方向和投影方式。 */ bool zoomIn(); /** @brief 按固定倍率 1.0 / 1.2 缩小当前视图。 */ bool zoomOut(); - /** @brief 保持当前观察方向和投影方式,仅适配可见边界。 */ + /** @brief 保持当前观察方向和投影方式,仅适配主内容或可见边界。 */ bool fitView(); /** @brief 沿 +Z 方向设置正交顶视图。 */ bool setTopView(); @@ -42,7 +47,7 @@ public: bool setIsometricView(); /** @brief 保存独立世界坐标旋转中心,不修改相机参数。 */ bool setRotationCenter(double dX, double dY, double dZ); - /** @brief 将独立旋转中心恢复为当前可见边界中心,不修改画面。 */ + /** @brief 将独立旋转中心恢复为主内容或可见边界中心,不修改画面。 */ bool resetRotationCenter(); /** @brief 返回是否已经设置有效的独立旋转中心。 */ bool hasRotationCenter() const; @@ -54,9 +59,13 @@ public: private: Q_DISABLE_COPY(nmVTKCameraController) + /** @brief 优先返回主内容边界,未设置时回退到可见 Prop 边界。 */ + bool getViewBounds(double dBounds[6]) const; /** @brief 读取并验证当前可见 Prop 的有限包围盒。 */ bool getVisibleBounds(double dBounds[6]) const; - /** @brief 设置观察方向和投影方式,再按可见边界适配相机。 */ + /** @brief 优先按全部可见 Prop 更新裁剪面,无可见边界时使用构图边界。 */ + void resetClippingRange(const double dViewBounds[6]); + /** @brief 设置观察方向和投影方式,再按主内容或可见边界适配相机。 */ bool applyStandardView(double dDirectionX, double dDirectionY, double dDirectionZ, @@ -68,6 +77,8 @@ private: bool zoom(double dFactor); vtkRenderer* m_pRenderer; ///< 非所有权指针,由渲染容器持有。 + bool m_bHasPrimaryViewBounds; + double m_dPrimaryViewBounds[6]; ///< 由业务控制器提供的主几何边界副本。 bool m_bHasRotationCenter; double m_dRotationCenter[3]; ///< 独立于 vtkCamera::FocalPoint 的世界坐标。 }; diff --git a/Include/nmNum/nmRender/nmVTKMeshLayer.h b/Include/nmNum/nmRender/nmVTKMeshLayer.h index bc41cfd8..0a226257 100644 --- a/Include/nmNum/nmRender/nmVTKMeshLayer.h +++ b/Include/nmNum/nmRender/nmVTKMeshLayer.h @@ -7,6 +7,7 @@ #include class vtkActor; +class vtkAlgorithmOutput; class vtkDataArray; class vtkDataSet; class vtkDataSetMapper; @@ -40,11 +41,29 @@ public: /** * @brief 共享传入数据集的 VTK 引用,不执行 DeepCopy。 - * @return 数据集非空且原标量选择在新数据集中仍有效时返回 true;没有 - * 原标量选择时只要求数据集非空。返回 false 时无效标量状态已清除。 + * @return 数据集非空且原标量选择在新数据集中仍有效时返回 true;失败时 + * 保留原输入、标量选择和 Mapper 状态。传入 nullptr 会清除输入。 */ bool setDataSet(vtkDataSet* pDataSet); - /** @brief 返回当前共享数据集的非所有权指针。 */ + /** + * @brief 连接通用 VTK 管线输出,不取得 Producer 所有权。 + * @return 输出为数据集且原标量选择仍有效时返回 true;失败时保留原输入。 + */ + bool setInputConnection(vtkAlgorithmOutput* pInputConnection); + /** + * @brief 原子提交管线输出、标量数组和显式显示范围。 + * + * 候选输出、数组、数据范围和显示范围会在 Mapper 改动前完整验证; + * 返回 false 时原输入、数组选择、范围和 LookupTable 均保持不变。 + */ + bool setInputConnection(vtkAlgorithmOutput* pInputConnection, + const QString& sArrayName, + ScalarAssociation eAssociation, + double dMinimum, + double dMaximum); + /** @brief 解除直接数据或管线连接,并清除当前标量选择。 */ + void clearInput(); + /** @brief 返回当前直接数据或管线输出数据集的非所有权指针。 */ vtkDataSet* getDataSet() const; /** @brief 设置表面或表面加边线显示模式。 */ @@ -72,6 +91,14 @@ public: */ bool setScalarArray(const QString& sArrayName, ScalarAssociation eAssociation); + /** + * @brief 原子提交当前输入的标量数组和显式显示范围。 + * @return 数组和两个范围均有效时返回 true;失败时保留原 Mapper 状态。 + */ + bool setScalarArray(const QString& sArrayName, + ScalarAssociation eAssociation, + double dMinimum, + double dMaximum); /** @brief 返回当前选中的数组名称。 */ QString getScalarArrayName() const; /** @brief 返回当前数组来自 CellData 还是 PointData。 */ @@ -92,6 +119,23 @@ public: private: Q_DISABLE_COPY(nmVTKMeshLayer) + /** @brief 解析直接输入或当前管线输出;按需先更新上游 Producer。 */ + vtkDataSet* getInputDataSet(bool bUpdatePipeline) const; + /** @brief 在不修改 Mapper 的前提下校验候选数据及保留的标量选择。 */ + bool validateInputDataSet(vtkDataSet* pDataSet, + double dScalarRange[2]) const; + /** @brief 查询指定关联中的数组,并验证其实际数据范围为有限值。 */ + vtkDataArray* validateScalarArray(vtkDataSet* pDataSet, + const QByteArray& aArrayName, + ScalarAssociation eAssociation, + double dDataRange[2]) const; + /** @brief 提交已经完整验证的数组选择和范围;本函数内部没有失败分支。 */ + void applyExplicitScalarState(const QByteArray& aArrayName, + ScalarAssociation eAssociation, + double dMinimum, + double dMaximum); + /** @brief 对新输入重新应用已经验证的标量模式、名称和自动范围。 */ + void applyRetainedScalarState(const double dScalarRange[2]); /** @brief 从当前数据集解析已记录的数组选择。 */ vtkDataArray* getSelectedScalarArray() const; /** @brief 校验数组范围并同步到 Mapper 和 LookupTable。 */ @@ -100,6 +144,7 @@ private: void clearInvalidScalarState(); vtkSmartPointer m_pDataSet; ///< 共享输入数据,不执行深拷贝。 + vtkSmartPointer m_pInputConnection; ///< 共享管线输出端口。 vtkSmartPointer m_pMapper; ///< 图层独占的映射器。 vtkSmartPointer m_pActor; ///< 图层主 Prop,由基类同步持有。 vtkSmartPointer m_pLookupTable; ///< 图层独占的颜色表。 diff --git a/Include/nmNum/nmRender/nmWxVTKRenderContainerWidget.h b/Include/nmNum/nmRender/nmWxVTKRenderContainerWidget.h index 69033976..96af9036 100644 --- a/Include/nmNum/nmRender/nmWxVTKRenderContainerWidget.h +++ b/Include/nmNum/nmRender/nmWxVTKRenderContainerWidget.h @@ -68,6 +68,11 @@ public: void setViewCommandCapabilities( nmVTKViewCommandController::ViewCommands eCommands); + /** @brief 设置相机适配、标准视图和重置中心优先使用的主内容边界。 */ + bool setPrimaryViewBounds(const double dBounds[6]); + /** @brief 清除主内容边界,使相机重新回退到全部可见 Prop 边界。 */ + void clearPrimaryViewBounds(); + /** @brief 注册唯一可拾取几何;传入 nullptr 会取消模式并隐藏中心标记。 */ void setRotationCenterPickTarget(vtkProp* pTarget); /** @brief 返回当前是否正在等待用户选择旋转中心。 */ @@ -90,7 +95,7 @@ public: * 供业务层批量替换几何时使用,调用方应在整批更新结束后统一 renderView。 */ bool fitTopViewWithoutRender(); - /** @brief 将独立旋转中心设为可见边界中心,但不修改画面或立即渲染。 */ + /** @brief 将独立旋转中心设为主内容或可见边界中心,不立即渲染。 */ bool resetRotationCenterWithoutRender(); /** @brief 切换到 VTK 页面并提交一次渲染。 */ void showReadyState(); @@ -115,9 +120,9 @@ public slots: void zoomIn(); /** @brief 按固定倍率 1.0 / 1.2 缩小。 */ void zoomOut(); - /** @brief 保持观察方向和投影方式,仅适配当前可见边界。 */ + /** @brief 保持观察方向和投影方式,仅适配主内容或可见边界。 */ void fitView(); - /** @brief 恢复透视投影,并按当前可见 Prop 的边界适配相机。 */ + /** @brief 恢复透视投影,并按主内容或当前可见 Prop 边界适配相机。 */ void resetCamera(); /** @brief 沿 +Z 方向设置正交顶视图。 */ void setTopView(); @@ -129,7 +134,7 @@ public slots: void setIsometricView(); /** @brief 设置独立世界坐标旋转中心,不移动当前画面。 */ void setRotationCenter(double dX, double dY, double dZ); - /** @brief 将旋转中心恢复为当前可见边界中心,不移动当前画面。 */ + /** @brief 将旋转中心恢复为主内容或当前可见边界中心,不移动画面。 */ void resetRotationCenter(); /** @brief 请求开始或取消一次性旋转中心拾取模式。 */ void setRotationCenterPickingEnabled(bool bEnabled); diff --git a/Include/nmNum/nmSubWxs/nmPebiResultSceneController.h b/Include/nmNum/nmSubWxs/nmPebiResultSceneController.h new file mode 100644 index 00000000..2ed71845 --- /dev/null +++ b/Include/nmNum/nmSubWxs/nmPebiResultSceneController.h @@ -0,0 +1,75 @@ +#ifndef NMPEBIRESULTSCENECONTROLLER_H +#define NMPEBIRESULTSCENECONTROLLER_H + +#include "nmSubWxs_global.h" + +#include +#include +#include + +class nmPebiResultSceneControllerPrivate; +class nmPebiResultSnapshot; +class nmWxVTKRenderContainerWidget; + +/** + * @brief 将不可变 PEBI 结果快照组合为独立的通用 VTK 场景。 + * + * Scene 拥有 Mesh、ScalarBar 和平面比例网格图层;控制器独占活动 + * RenderAdapter 和快照强引用,并通过稳定 ID 校验所观察的图层身份。 + * 外部提前清空业务 Scene 时,控制器会丢弃活动结果而不会解引用旧图层。 + */ +class NM_SUB_WXS_EXPORT nmPebiResultSceneController +{ +public: + /** + * @param pContainer 非所有权指针,必须比控制器生命周期更长。 + * @note 析构会从业务 Scene 移除本控制器创建的稳定 ID 图层。 + */ + explicit nmPebiResultSceneController( + nmWxVTKRenderContainerWidget* pContainer); + ~nmPebiResultSceneController(); + + /** + * @brief 事务式绑定新快照,成功后恢复顶视图、适配网格并渲染一次。 + * @note 候选初始化失败时保留当前 Adapter、画面、范围和相机状态; + * 若发现 Scene 已由外部清空,则采用一致的无快照状态。 + */ + bool setSnapshot( + const QSharedPointer& pSnapshot, + QString* pError = nullptr); + /** @brief 解除管线和快照引用并隐藏结果图层,不销毁 Scene。 */ + void clear(); + + /** + * @brief 原子切换同一快照内的属性和帧,成功时仅渲染一次。 + * @note 静态属性只接受帧 0;失败时恢复 Adapter、Mapper 和范围。 + */ + bool applyPropertyFrame(const QString& sPropertyId, + int nFrameIndex, + QString* pError = nullptr); + + /** 以下显示设置会作为偏好保留,并在存在活动快照时渲染一次。 */ + void setMeshVisible(bool bVisible); + void setScalarBarVisible(bool bVisible); + void setScaleGridVisible(bool bVisible); + void setCellEdgesVisible(bool bVisible); + + bool hasSnapshot() const; + QString getCurrentSnapshotId() const; + QStringList getAvailablePropertyIds() const; + QString getCurrentPropertyId() const; + int getCurrentFrameIndex() const; + /** @brief 返回快照中的业务范围;常量属性保持 [c,c]。 */ + bool getCurrentPropertyRange(double dRange[2]) const; + bool isMeshVisible() const; + bool isScalarBarVisible() const; + bool isScaleGridVisible() const; + bool areCellEdgesVisible() const; + +private: + Q_DISABLE_COPY(nmPebiResultSceneController) + + nmPebiResultSceneControllerPrivate* m_pPrivate; ///< 独占实现对象。 +}; + +#endif // NMPEBIRESULTSCENECONTROLLER_H diff --git a/Src/nmNum/nmData/nmPebiResultSnapshotRenderAdapter.cpp b/Src/nmNum/nmData/nmPebiResultSnapshotRenderAdapter.cpp index abf8a45b..006f883b 100644 --- a/Src/nmNum/nmData/nmPebiResultSnapshotRenderAdapter.cpp +++ b/Src/nmNum/nmData/nmPebiResultSnapshotRenderAdapter.cpp @@ -208,6 +208,16 @@ QString nmPebiResultSnapshotRenderAdapter::getCurrentPropertyId() const return m_sCurrentPropertyId; } +QString nmPebiResultSnapshotRenderAdapter::getCurrentScalarArrayName() const +{ + if(m_sCurrentPropertyId.isEmpty()) + { + return QString(); + } + return m_sCurrentPropertyId == QString::fromLatin1("pressure") + ? QString::fromLatin1("p") : m_sCurrentPropertyId; +} + vtkIdType nmPebiResultSnapshotRenderAdapter::getCellCount() const { return m_pRenderGrid == NULL ? 0 : diff --git a/Src/nmNum/nmRender/nmVTKCameraController.cpp b/Src/nmNum/nmRender/nmVTKCameraController.cpp index b4f3efb6..cff80f07 100644 --- a/Src/nmNum/nmRender/nmVTKCameraController.cpp +++ b/Src/nmNum/nmRender/nmVTKCameraController.cpp @@ -21,9 +21,13 @@ bool isCameraGuiThread() nmVTKCameraController::nmVTKCameraController(vtkRenderer* pRenderer) : m_pRenderer(pRenderer), + m_bHasPrimaryViewBounds(false), m_bHasRotationCenter(false) { Q_ASSERT(isCameraGuiThread()); + for(int nIndex = 0; nIndex < 6; ++nIndex) { + m_dPrimaryViewBounds[nIndex] = 0.0; + } m_dRotationCenter[0] = 0.0; m_dRotationCenter[1] = 0.0; m_dRotationCenter[2] = 0.0; @@ -41,6 +45,7 @@ void nmVTKCameraController::setRenderer(vtkRenderer* pRenderer) Q_ASSERT(isCameraGuiThread()); if(isCameraGuiThread()) { if(m_pRenderer != pRenderer) { + clearPrimaryViewBounds(); clearRotationCenter(); } m_pRenderer = pRenderer; @@ -52,11 +57,43 @@ vtkRenderer* nmVTKCameraController::getRenderer() const return m_pRenderer; } +bool nmVTKCameraController::setPrimaryViewBounds( + const double dBounds[6]) +{ + Q_ASSERT(isCameraGuiThread()); + if(!isCameraGuiThread() || dBounds == nullptr) { + return false; + } + for(int nIndex = 0; nIndex < 6; ++nIndex) { + if(!vtkMath::IsFinite(dBounds[nIndex])) { + return false; + } + } + if(dBounds[0] > dBounds[1] || dBounds[2] > dBounds[3] || + dBounds[4] > dBounds[5]) { + return false; + } + + for(int nIndex = 0; nIndex < 6; ++nIndex) { + m_dPrimaryViewBounds[nIndex] = dBounds[nIndex]; + } + m_bHasPrimaryViewBounds = true; + return true; +} + +void nmVTKCameraController::clearPrimaryViewBounds() +{ + Q_ASSERT(isCameraGuiThread()); + if(isCameraGuiThread()) { + m_bHasPrimaryViewBounds = false; + } +} + bool nmVTKCameraController::resetCamera() { Q_ASSERT(isCameraGuiThread()); double dBounds[6]; - if(!isCameraGuiThread() || !getVisibleBounds(dBounds)) { + if(!isCameraGuiThread() || !getViewBounds(dBounds)) { return false; } @@ -64,10 +101,10 @@ bool nmVTKCameraController::resetCamera() if(pCamera == nullptr) { return false; } - // 重置入口恢复默认三维透视投影,再按当前可见边界适配。 + // 重置入口恢复默认三维透视投影,再按主内容或当前可见边界适配。 pCamera->ParallelProjectionOff(); m_pRenderer->ResetCamera(dBounds); - m_pRenderer->ResetCameraClippingRange(dBounds); + resetClippingRange(dBounds); return true; } @@ -85,7 +122,7 @@ bool nmVTKCameraController::fitView() { Q_ASSERT(isCameraGuiThread()); double dBounds[6]; - if(!isCameraGuiThread() || !getVisibleBounds(dBounds)) { + if(!isCameraGuiThread() || !getViewBounds(dBounds)) { return false; } @@ -96,7 +133,7 @@ bool nmVTKCameraController::fitView() // ResetCamera 只重新适配边界,不改写调用方选定的观察方向和投影方式。 m_pRenderer->ResetCamera(dBounds); - m_pRenderer->ResetCameraClippingRange(dBounds); + resetClippingRange(dBounds); return true; } @@ -148,7 +185,7 @@ bool nmVTKCameraController::resetRotationCenter() { Q_ASSERT(isCameraGuiThread()); double dBounds[6]; - if(!isCameraGuiThread() || !getVisibleBounds(dBounds)) { + if(!isCameraGuiThread() || !getViewBounds(dBounds)) { return false; } // 只更新独立枢轴;当前相机 Position/FocalPoint/ViewUp 保持不变。 @@ -178,6 +215,22 @@ void nmVTKCameraController::clearRotationCenter() m_bHasRotationCenter = false; } +bool nmVTKCameraController::getViewBounds(double dBounds[6]) const +{ + // 主内容边界虽然是值副本,相机操作仍必须依赖有效 Renderer。 + // 这里统一守住空指针,避免设置过主边界后绕过 getVisibleBounds 的校验。 + if(m_pRenderer == nullptr || dBounds == nullptr) { + return false; + } + if(!m_bHasPrimaryViewBounds) { + return getVisibleBounds(dBounds); + } + for(int nIndex = 0; nIndex < 6; ++nIndex) { + dBounds[nIndex] = m_dPrimaryViewBounds[nIndex]; + } + return true; +} + bool nmVTKCameraController::getVisibleBounds(double dBounds[6]) const { if(m_pRenderer == nullptr || dBounds == nullptr) { @@ -199,6 +252,24 @@ bool nmVTKCameraController::getVisibleBounds(double dBounds[6]) const dBounds[4] <= dBounds[5]; } +void nmVTKCameraController::resetClippingRange( + const double dViewBounds[6]) +{ + if(m_pRenderer == nullptr || dViewBounds == nullptr) { + return; + } + double dVisibleBounds[6]; + if(getVisibleBounds(dVisibleBounds)) { + // 裁剪范围可以包含装饰层,但相机位置和缩放仍只由主内容决定。 + m_pRenderer->ResetCameraClippingRange(dVisibleBounds); + } else { + m_pRenderer->ResetCameraClippingRange( + dViewBounds[0], dViewBounds[1], + dViewBounds[2], dViewBounds[3], + dViewBounds[4], dViewBounds[5]); + } +} + bool nmVTKCameraController::applyStandardView( double dDirectionX, double dDirectionY, @@ -210,7 +281,7 @@ bool nmVTKCameraController::applyStandardView( { Q_ASSERT(isCameraGuiThread()); double dBounds[6]; - if(!isCameraGuiThread() || !getVisibleBounds(dBounds)) { + if(!isCameraGuiThread() || !getViewBounds(dBounds)) { return false; } @@ -248,7 +319,7 @@ bool nmVTKCameraController::applyStandardView( pCamera->SetParallelProjection(bParallelProjection ? 1 : 0); pCamera->OrthogonalizeViewUp(); m_pRenderer->ResetCamera(dBounds); - m_pRenderer->ResetCameraClippingRange(dBounds); + resetClippingRange(dBounds); return true; } diff --git a/Src/nmNum/nmRender/nmVTKMeshLayer.cpp b/Src/nmNum/nmRender/nmVTKMeshLayer.cpp index d19153c0..6b88d376 100644 --- a/Src/nmNum/nmRender/nmVTKMeshLayer.cpp +++ b/Src/nmNum/nmRender/nmVTKMeshLayer.cpp @@ -5,8 +5,11 @@ #include #include +#include +#include #include #include +#include #include #include #include @@ -29,6 +32,7 @@ bool isMeshLayerGuiThread() nmVTKMeshLayer::nmVTKMeshLayer(const QString& sLayerId) : nmVTKRenderLayer(sLayerId), m_pDataSet(nullptr), + m_pInputConnection(nullptr), m_pMapper(vtkSmartPointer::New()), m_pActor(vtkSmartPointer::New()), m_pLookupTable(vtkSmartPointer::New()), @@ -56,7 +60,9 @@ nmVTKMeshLayer::~nmVTKMeshLayer() Q_ASSERT(isMeshLayerGuiThread()); // 派生成员释放前先解除 Renderer,基类析构中的 detach 仍保持幂等。 detach(); + clearInput(); m_pDataSet = nullptr; + m_pInputConnection = nullptr; } bool nmVTKMeshLayer::setDataSet(vtkDataSet* pDataSet) @@ -65,41 +71,105 @@ bool nmVTKMeshLayer::setDataSet(vtkDataSet* pDataSet) if(!isMeshLayerGuiThread()) { return false; } + if(pDataSet == nullptr) { + clearInput(); + return false; + } - // vtkSmartPointer 仅增加引用计数,不复制点、单元或属性数组。 - m_pDataSet = pDataSet; + double dScalarRange[2] = { 0.0, 1.0 }; + if(!validateInputDataSet(pDataSet, dScalarRange)) { + return false; + } + + // 所有可能失败的校验均在替换 Mapper 输入前完成。 m_pMapper->SetInputData(pDataSet); + m_pDataSet = pDataSet; + m_pInputConnection = nullptr; + applyRetainedScalarState(dScalarRange); + return true; +} - if(pDataSet == nullptr) { - clearInvalidScalarState(); +bool nmVTKMeshLayer::setInputConnection( + vtkAlgorithmOutput* pInputConnection) +{ + Q_ASSERT(isMeshLayerGuiThread()); + if(!isMeshLayerGuiThread() || pInputConnection == nullptr || + pInputConnection->GetProducer() == nullptr || + pInputConnection->GetIndex() < 0) { return false; } - // 更换数据集时保留调用方此前的数组选择;若同名数组存在则恢复其映射方式。 - if(!m_aScalarArrayName.isEmpty()) { - if(getSelectedScalarArray() == nullptr) { - clearInvalidScalarState(); - return false; - } - if(m_eScalarAssociation == ScalarAssociation_CellData) { - m_pMapper->SetScalarModeToUseCellFieldData(); - } else { - m_pMapper->SetScalarModeToUsePointFieldData(); - } - m_pMapper->SelectColorArray(m_aScalarArrayName.constData()); - if(!m_bUsingExplicitScalarRange) { - if(!applyDataScalarRange()) { - clearInvalidScalarState(); - return false; - } - } + vtkAlgorithm* pProducer = pInputConnection->GetProducer(); + pProducer->Update(pInputConnection->GetIndex()); + vtkDataSet* pCandidateDataSet = vtkDataSet::SafeDownCast( + pProducer->GetOutputDataObject( + pInputConnection->GetIndex())); + double dScalarRange[2] = { 0.0, 1.0 }; + if(!validateInputDataSet(pCandidateDataSet, dScalarRange)) { + return false; } + + // 输出对象、保留数组和自动范围均已验证,此后才替换活动连接。 + m_pMapper->SetInputConnection(pInputConnection); + m_pInputConnection = pInputConnection; + m_pDataSet = nullptr; + applyRetainedScalarState(dScalarRange); + return true; +} + +bool nmVTKMeshLayer::setInputConnection( + vtkAlgorithmOutput* pInputConnection, + const QString& sArrayName, + ScalarAssociation eAssociation, + double dMinimum, + double dMaximum) +{ + Q_ASSERT(isMeshLayerGuiThread()); + if(!isMeshLayerGuiThread() || pInputConnection == nullptr || + pInputConnection->GetProducer() == nullptr || + pInputConnection->GetIndex() < 0 || sArrayName.isEmpty() || + !vtkMath::IsFinite(dMinimum) || !vtkMath::IsFinite(dMaximum) || + dMinimum > dMaximum) { + return false; + } + + vtkAlgorithm* pProducer = pInputConnection->GetProducer(); + const int nOutputPort = pInputConnection->GetIndex(); + pProducer->Update(nOutputPort); + vtkDataSet* pCandidateDataSet = vtkDataSet::SafeDownCast( + pProducer->GetOutputDataObject(nOutputPort)); + const QByteArray aArrayName = sArrayName.toUtf8(); + double dDataRange[2] = { 0.0, 0.0 }; + if(validateScalarArray(pCandidateDataSet, aArrayName, + eAssociation, dDataRange) == nullptr) { + return false; + } + + // 到这里所有可能失败的条件均已排除。下面只执行 VTK 无返回值 setter, + // 因而输入、数组和范围会作为一个不可分割的 Mapper 状态提交。 + m_pMapper->SetInputConnection(pInputConnection); + m_pInputConnection = pInputConnection; + m_pDataSet = nullptr; + applyExplicitScalarState(aArrayName, eAssociation, + dMinimum, dMaximum); return true; } +void nmVTKMeshLayer::clearInput() +{ + Q_ASSERT(isMeshLayerGuiThread()); + if(!isMeshLayerGuiThread() || m_pMapper == nullptr) { + return; + } + m_pMapper->RemoveAllInputs(); + m_pInputConnection = nullptr; + m_pDataSet = nullptr; + clearInvalidScalarState(); +} + vtkDataSet* nmVTKMeshLayer::getDataSet() const { - return m_pDataSet; + return getInputDataSet(false); } void nmVTKMeshLayer::setDisplayMode(DisplayMode eMode) @@ -170,32 +240,20 @@ bool nmVTKMeshLayer::setScalarArray( ScalarAssociation eAssociation) { Q_ASSERT(isMeshLayerGuiThread()); + vtkDataSet* pDataSet = getInputDataSet(true); if(!isMeshLayerGuiThread() || sArrayName.isEmpty() || - m_pDataSet == nullptr || m_pMapper == nullptr || - (eAssociation != ScalarAssociation_CellData && - eAssociation != ScalarAssociation_PointData)) { + pDataSet == nullptr || m_pMapper == nullptr) { return false; } const QByteArray aArrayName = sArrayName.toUtf8(); - vtkDataArray* pArray = eAssociation == ScalarAssociation_CellData - ? m_pDataSet->GetCellData()->GetArray(aArrayName.constData()) - : m_pDataSet->GetPointData()->GetArray(aArrayName.constData()); - if(pArray == nullptr) { + double dRange[2] = { 0.0, 0.0 }; + if(validateScalarArray(pDataSet, aArrayName, + eAssociation, dRange) == nullptr) { // 失败时保留原数组选择和原 Mapper 状态。 return false; } - double dRange[2] = { 0.0, 0.0 }; - if(!m_bUsingExplicitScalarRange) { - pArray->GetRange(dRange); - if(!vtkMath::IsFinite(dRange[0]) || - !vtkMath::IsFinite(dRange[1]) || dRange[0] > dRange[1]) { - // 自动范围无效时不提交新数组选择,避免形成半更新状态。 - return false; - } - } - m_aScalarArrayName = aArrayName; m_eScalarAssociation = eAssociation; if(eAssociation == ScalarAssociation_CellData) { @@ -214,6 +272,34 @@ bool nmVTKMeshLayer::setScalarArray( return true; } +bool nmVTKMeshLayer::setScalarArray( + const QString& sArrayName, + ScalarAssociation eAssociation, + double dMinimum, + double dMaximum) +{ + Q_ASSERT(isMeshLayerGuiThread()); + vtkDataSet* pDataSet = getInputDataSet(true); + if(!isMeshLayerGuiThread() || sArrayName.isEmpty() || + pDataSet == nullptr || m_pMapper == nullptr || + !vtkMath::IsFinite(dMinimum) || !vtkMath::IsFinite(dMaximum) || + dMinimum > dMaximum) { + return false; + } + + const QByteArray aArrayName = sArrayName.toUtf8(); + double dDataRange[2] = { 0.0, 0.0 }; + if(validateScalarArray(pDataSet, aArrayName, + eAssociation, dDataRange) == nullptr) { + return false; + } + + // 数组和范围都已验证,提交阶段不会再产生可观察的半更新状态。 + applyExplicitScalarState(aArrayName, eAssociation, + dMinimum, dMaximum); + return true; +} + QString nmVTKMeshLayer::getScalarArrayName() const { return QString::fromUtf8(m_aScalarArrayName.constData()); @@ -273,15 +359,117 @@ vtkLookupTable* nmVTKMeshLayer::getLookupTable() const return m_pLookupTable; } +vtkDataSet* nmVTKMeshLayer::getInputDataSet(bool bUpdatePipeline) const +{ + if(m_pInputConnection == nullptr) { + return m_pDataSet; + } + + vtkAlgorithm* pProducer = m_pInputConnection->GetProducer(); + const int nOutputPort = m_pInputConnection->GetIndex(); + if(pProducer == nullptr || nOutputPort < 0) { + return nullptr; + } + if(bUpdatePipeline) { + pProducer->Update(nOutputPort); + } + return vtkDataSet::SafeDownCast( + pProducer->GetOutputDataObject(nOutputPort)); +} + +bool nmVTKMeshLayer::validateInputDataSet( + vtkDataSet* pDataSet, + double dScalarRange[2]) const +{ + if(pDataSet == nullptr || dScalarRange == nullptr) { + return false; + } + if(m_aScalarArrayName.isEmpty()) { + return true; + } + + return validateScalarArray(pDataSet, m_aScalarArrayName, + m_eScalarAssociation, + dScalarRange) != nullptr; +} + +vtkDataArray* nmVTKMeshLayer::validateScalarArray( + vtkDataSet* pDataSet, + const QByteArray& aArrayName, + ScalarAssociation eAssociation, + double dDataRange[2]) const +{ + if(pDataSet == nullptr || aArrayName.isEmpty() || + dDataRange == nullptr || + (eAssociation != ScalarAssociation_CellData && + eAssociation != ScalarAssociation_PointData)) { + return nullptr; + } + + vtkDataArray* pArray = eAssociation == ScalarAssociation_CellData + ? pDataSet->GetCellData()->GetArray(aArrayName.constData()) + : pDataSet->GetPointData()->GetArray(aArrayName.constData()); + if(pArray == nullptr) { + return nullptr; + } + pArray->GetRange(dDataRange); + return vtkMath::IsFinite(dDataRange[0]) && + vtkMath::IsFinite(dDataRange[1]) && + dDataRange[0] <= dDataRange[1] + ? pArray : nullptr; +} + +void nmVTKMeshLayer::applyExplicitScalarState( + const QByteArray& aArrayName, + ScalarAssociation eAssociation, + double dMinimum, + double dMaximum) +{ + m_aScalarArrayName = aArrayName; + m_eScalarAssociation = eAssociation; + if(eAssociation == ScalarAssociation_CellData) { + m_pMapper->SetScalarModeToUseCellFieldData(); + } else { + m_pMapper->SetScalarModeToUsePointFieldData(); + } + m_pMapper->SelectColorArray(m_aScalarArrayName.constData()); + m_pMapper->SetScalarRange(dMinimum, dMaximum); + m_pLookupTable->SetRange(dMinimum, dMaximum); + m_pLookupTable->Build(); + m_bUsingExplicitScalarRange = true; +} + +void nmVTKMeshLayer::applyRetainedScalarState( + const double dScalarRange[2]) +{ + if(m_aScalarArrayName.isEmpty()) { + return; + } + if(m_eScalarAssociation == ScalarAssociation_CellData) { + m_pMapper->SetScalarModeToUseCellFieldData(); + } else { + m_pMapper->SetScalarModeToUsePointFieldData(); + } + m_pMapper->SelectColorArray(m_aScalarArrayName.constData()); + if(!m_bUsingExplicitScalarRange && dScalarRange != nullptr) { + m_pMapper->SetScalarRange( + dScalarRange[0], dScalarRange[1]); + m_pLookupTable->SetRange( + dScalarRange[0], dScalarRange[1]); + m_pLookupTable->Build(); + } +} + vtkDataArray* nmVTKMeshLayer::getSelectedScalarArray() const { - if(m_pDataSet == nullptr || m_aScalarArrayName.isEmpty()) { + vtkDataSet* pDataSet = getInputDataSet(true); + if(pDataSet == nullptr || m_aScalarArrayName.isEmpty()) { return nullptr; } return m_eScalarAssociation == ScalarAssociation_CellData - ? m_pDataSet->GetCellData()->GetArray( + ? pDataSet->GetCellData()->GetArray( m_aScalarArrayName.constData()) - : m_pDataSet->GetPointData()->GetArray( + : pDataSet->GetPointData()->GetArray( m_aScalarArrayName.constData()); } diff --git a/Src/nmNum/nmRender/nmWxVTKRenderContainerWidget.cpp b/Src/nmNum/nmRender/nmWxVTKRenderContainerWidget.cpp index df02ab39..c00c673b 100644 --- a/Src/nmNum/nmRender/nmWxVTKRenderContainerWidget.cpp +++ b/Src/nmNum/nmRender/nmWxVTKRenderContainerWidget.cpp @@ -288,6 +288,22 @@ void nmWxVTKRenderContainerWidget::setViewCommandCapabilities( updateRotationCenterMarkerVisibility(); } +bool nmWxVTKRenderContainerWidget::setPrimaryViewBounds( + const double dBounds[6]) +{ + Q_ASSERT(isGuiThread()); + return isGuiThread() && m_pCameraController != nullptr && + m_pCameraController->setPrimaryViewBounds(dBounds); +} + +void nmWxVTKRenderContainerWidget::clearPrimaryViewBounds() +{ + Q_ASSERT(isGuiThread()); + if(isGuiThread() && m_pCameraController != nullptr) { + m_pCameraController->clearPrimaryViewBounds(); + } +} + void nmWxVTKRenderContainerWidget::setRotationCenterPickTarget( vtkProp* pTarget) { diff --git a/Src/nmNum/nmSubWxs/nmPebiResultSceneController.cpp b/Src/nmNum/nmSubWxs/nmPebiResultSceneController.cpp new file mode 100644 index 00000000..b5168c43 --- /dev/null +++ b/Src/nmNum/nmSubWxs/nmPebiResultSceneController.cpp @@ -0,0 +1,1031 @@ +#include "nmPebiResultSceneController.h" + +#include "nmPebiResultPropertyCatalog.h" +#include "nmPebiResultSnapshot.h" +#include "nmPebiResultSnapshotRenderAdapter.h" + +#include "nmVTKMeshLayer.h" +#include "nmVTKPlanarScaleGridLayer.h" +#include "nmVTKScalarBarLayer.h" +#include "nmVTKScene.h" +#include "nmWxVTKRenderContainerWidget.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace { + +const char* const RESULT_MESH_LAYER_ID = "pebi.result.mesh"; +const char* const RESULT_SCALAR_BAR_LAYER_ID = "pebi.result.scalar_bar"; +const char* const RESULT_SCALE_GRID_LAYER_ID = "pebi.result.scale_grid"; +const char* const RESULT_WELLS_LAYER_ID = "pebi.result.wells"; + +/* Qt 4.8 的 lupdate 无法从变量参数中推导翻译原文,必须显式登记。 */ +static const char* const s_pResultSceneTranslationMarkers[] = +{ + QT_TRANSLATE_NOOP("nmPebiResultSceneController", + "The result snapshot is invalid."), + QT_TRANSLATE_NOOP("nmPebiResultSceneController", + "The result snapshot has no renderable property."), + QT_TRANSLATE_NOOP("nmPebiResultSceneController", + "Unable to initialize the result render adapter."), + QT_TRANSLATE_NOOP("nmPebiResultSceneController", + "The result render data failed validation."), + QT_TRANSLATE_NOOP("nmPebiResultSceneController", + "Unable to create the result render layers."), + QT_TRANSLATE_NOOP("nmPebiResultSceneController", + "Unable to commit the result render data."), + QT_TRANSLATE_NOOP("nmPebiResultSceneController", + "The requested result property or frame is invalid."), + QT_TRANSLATE_NOOP("nmPebiResultSceneController", + "The result property range is invalid."), + QT_TRANSLATE_NOOP("nmPebiResultSceneController", + "Unable to apply the result property or frame.") +}; + +bool isResultSceneGuiThread() +{ + QCoreApplication* pApplication = QCoreApplication::instance(); + return pApplication == nullptr || + pApplication->thread() == QThread::currentThread(); +} + +QString controllerText(const char* pText) +{ + Q_UNUSED(s_pResultSceneTranslationMarkers); + return QCoreApplication::translate( + "nmPebiResultSceneController", pText); +} + +void copyBounds(const double dSource[6], double dTarget[6]) +{ + for(int nIndex = 0; nIndex < 6; ++nIndex) { + dTarget[nIndex] = dSource[nIndex]; + } +} + +bool getFiniteBounds(vtkDataSet* pDataSet, double dBounds[6]) +{ + if(pDataSet == nullptr || dBounds == nullptr) { + return false; + } + pDataSet->GetBounds(dBounds); + if(!vtkMath::AreBoundsInitialized(dBounds)) { + return false; + } + for(int nIndex = 0; nIndex < 6; ++nIndex) { + if(!vtkMath::IsFinite(dBounds[nIndex])) { + return false; + } + } + return dBounds[0] < dBounds[1] && + dBounds[2] < dBounds[3] && + dBounds[4] <= dBounds[5]; +} + +bool getDisplayRange(const double dBusinessRange[2], + double dDisplayRange[2]) +{ + if(dBusinessRange == nullptr || dDisplayRange == nullptr || + !vtkMath::IsFinite(dBusinessRange[0]) || + !vtkMath::IsFinite(dBusinessRange[1]) || + dBusinessRange[0] > dBusinessRange[1]) { + return false; + } + + dDisplayRange[0] = dBusinessRange[0]; + dDisplayRange[1] = dBusinessRange[1]; + if(dBusinessRange[0] < dBusinessRange[1]) { + return true; + } + + // VTK 需要非零显示区间;业务层仍保存并返回原始常量范围 [c,c]。 + const double dValue = dBusinessRange[0]; + const double dEpsilon = qMax(1.0, fabs(dValue)) * 1.0e-6; + double dMinimum = dValue - dEpsilon; + double dMaximum = dValue + dEpsilon; + if(!vtkMath::IsFinite(dMinimum)) { + dMinimum = dValue; + } + if(!vtkMath::IsFinite(dMaximum)) { + dMaximum = dValue; + } + if(dMinimum >= dMaximum) { + return false; + } + dDisplayRange[0] = dMinimum; + dDisplayRange[1] = dMaximum; + return true; +} + +bool getValidatedPropertyRanges( + const QSharedPointer& pSnapshot, + const QString& sPropertyId, + double dBusinessRange[2], + double dDisplayRange[2]) +{ + return !pSnapshot.isNull() && + pSnapshot->getPropertyRange(sPropertyId, dBusinessRange) && + getDisplayRange(dBusinessRange, dDisplayRange); +} + +QString chooseInitialProperty( + const QSharedPointer& pSnapshot) +{ + const QString sPressure = QString::fromLatin1("pressure"); + if(!pSnapshot.isNull() && pSnapshot->hasProperty(sPressure) && + pSnapshot->getPropertyFrameCount(sPressure) > 0) { + return sPressure; + } + + // 无压力仅是防御分支;按 Catalog 顺序选择第一个可用静态属性。 + const QList listDescriptors = + nmPebiResultPropertyCatalog::descriptors(); + for(int nIndex = 0; nIndex < listDescriptors.size(); ++nIndex) { + const nmPebiResultPropertyDescriptor& oDescriptor = + listDescriptors[nIndex]; + if(oDescriptor.m_eTimeType == NM_PebiResultProperty_Static && + pSnapshot->hasProperty(oDescriptor.m_sPropertyId)) { + return oDescriptor.m_sPropertyId; + } + } + return QString(); +} + +bool areSnapshotWellsValid( + const QSharedPointer& pSnapshot) +{ + if(pSnapshot.isNull()) { + return false; + } + QSet setWellIds; + for(int nIndex = 0; nIndex < pSnapshot->getWellCount(); ++nIndex) { + const nmPebiResultWellSnapshot* pWell = pSnapshot->getWellAt(nIndex); + if(pWell == nullptr || pWell->m_sWellInstanceId.isEmpty() || + pWell->m_sWellCode.isEmpty() || + !vtkMath::IsFinite(pWell->m_oLocation.x()) || + !vtkMath::IsFinite(pWell->m_oLocation.y()) || + setWellIds.contains(pWell->m_sWellInstanceId)) { + return false; + } + setWellIds.insert(pWell->m_sWellInstanceId); + } + return true; +} + +bool validateAdapterOutput( + nmPebiResultSnapshotRenderAdapter* pAdapter, + vtkIdType nExpectedCellCount, + double dBounds[6]) +{ + if(pAdapter == nullptr || dBounds == nullptr || + pAdapter->getCellCount() != nExpectedCellCount || + nExpectedCellCount <= 0) { + return false; + } + + vtkAlgorithmOutput* pOutputPort = pAdapter->getOutputPort(); + if(pOutputPort == nullptr || pOutputPort->GetProducer() == nullptr || + pOutputPort->GetIndex() < 0) { + return false; + } + vtkAlgorithm* pProducer = pOutputPort->GetProducer(); + pProducer->Update(pOutputPort->GetIndex()); + vtkDataSet* pDataSet = vtkDataSet::SafeDownCast( + pProducer->GetOutputDataObject(pOutputPort->GetIndex())); + if(pDataSet == nullptr || + pDataSet->GetNumberOfCells() != nExpectedCellCount || + !getFiniteBounds(pDataSet, dBounds)) { + return false; + } + + const QByteArray aScalarName = + pAdapter->getCurrentScalarArrayName().toUtf8(); + vtkDataArray* pScalars = aScalarName.isEmpty() ? nullptr : + pDataSet->GetCellData()->GetArray(aScalarName.constData()); + if(pScalars == nullptr || pScalars->GetNumberOfComponents() != 1 || + pScalars->GetNumberOfTuples() != nExpectedCellCount) { + return false; + } + double dActualRange[2] = { 0.0, 0.0 }; + pScalars->GetRange(dActualRange); + return vtkMath::IsFinite(dActualRange[0]) && + vtkMath::IsFinite(dActualRange[1]) && + dActualRange[0] <= dActualRange[1]; +} + +} + +class nmPebiResultSceneControllerPrivate +{ +public: + explicit nmPebiResultSceneControllerPrivate( + nmWxVTKRenderContainerWidget* pContainer) + : m_pContainer(pContainer), + m_pMeshLayer(nullptr), + m_pScalarBarLayer(nullptr), + m_pScaleGridLayer(nullptr), + m_pAdapter(nullptr), + m_nCurrentFrameIndex(-1), + m_bHasPropertyRange(false), + m_bHasBounds(false), + m_bMeshVisible(true), + // 5C 补齐属性名和单位标题前,避免显示语义不完整的色标。 + m_bScalarBarVisible(false), + m_bScaleGridVisible(false), + m_bCellEdgesVisible(false) + { + m_dBusinessRange[0] = 0.0; + m_dBusinessRange[1] = 0.0; + m_dDisplayRange[0] = 0.0; + m_dDisplayRange[1] = 1.0; + for(int nIndex = 0; nIndex < 6; ++nIndex) { + m_dBounds[nIndex] = 0.0; + } + // 5A 固定保留该 ID,井位图层在 5C 才创建。 + Q_UNUSED(RESULT_WELLS_LAYER_ID); + } + + nmVTKScene* getScene() const + { + return m_pContainer != nullptr ? m_pContainer->getScene() : nullptr; + } + + bool synchronizeLayerPointers() + { + nmVTKScene* pScene = getScene(); + bool bPointersIntact = pScene != nullptr; + if(pScene == nullptr) { + bPointersIntact = m_pMeshLayer == nullptr && + m_pScalarBarLayer == nullptr && + m_pScaleGridLayer == nullptr; + m_pMeshLayer = nullptr; + m_pScalarBarLayer = nullptr; + m_pScaleGridLayer = nullptr; + return bPointersIntact; + } + + // Scene 是图层唯一所有者。这里只比较指针身份,不解引用可能已经由 + // 外部 Scene::clear() 删除的旧地址;不一致时立即丢弃观察指针。 + if(m_pMeshLayer != nullptr && + pScene->getLayer(QString::fromLatin1(RESULT_MESH_LAYER_ID)) != + m_pMeshLayer) { + m_pMeshLayer = nullptr; + bPointersIntact = false; + } + if(m_pScalarBarLayer != nullptr && + pScene->getLayer( + QString::fromLatin1(RESULT_SCALAR_BAR_LAYER_ID)) != + m_pScalarBarLayer) { + m_pScalarBarLayer = nullptr; + bPointersIntact = false; + } + if(m_pScaleGridLayer != nullptr && + pScene->getLayer( + QString::fromLatin1(RESULT_SCALE_GRID_LAYER_ID)) != + m_pScaleGridLayer) { + m_pScaleGridLayer = nullptr; + bPointersIntact = false; + } + return bPointersIntact; + } + + bool hasOwnedBaseLayers() const + { + if(!isResultSceneGuiThread()) { + return false; + } + nmVTKScene* pScene = getScene(); + return pScene != nullptr && + m_pMeshLayer != nullptr && + m_pScalarBarLayer != nullptr && + m_pScaleGridLayer != nullptr && + pScene->getLayer( + QString::fromLatin1(RESULT_MESH_LAYER_ID)) == + m_pMeshLayer && + pScene->getLayer( + QString::fromLatin1(RESULT_SCALAR_BAR_LAYER_ID)) == + m_pScalarBarLayer && + pScene->getLayer( + QString::fromLatin1(RESULT_SCALE_GRID_LAYER_ID)) == + m_pScaleGridLayer; + } + + void discardActiveDataIfLayersLost() + { + const bool bPointersIntact = synchronizeLayerPointers(); + const bool bHasActiveState = m_pAdapter != nullptr || + !m_pSnapshot.isNull(); + if(bHasActiveState && + (!bPointersIntact || !hasOwnedBaseLayers())) { + // 外部已经清理业务 Scene 时无法继续兑现“当前快照可见”的状态, + // 因此采用 fail-closed:释放逻辑状态,但绝不访问已删除的 Layer。 + releaseActiveData(); + } + } + + bool ensureBaseLayers(bool& bCreatedMesh, + bool& bCreatedScalarBar, + bool& bCreatedScaleGrid) + { + bCreatedMesh = false; + bCreatedScalarBar = false; + bCreatedScaleGrid = false; + const bool bPointersIntact = synchronizeLayerPointers(); + nmVTKScene* pScene = getScene(); + vtkRenderer* pRenderer = m_pContainer != nullptr + ? m_pContainer->getRenderer() : nullptr; + if(pScene == nullptr || pRenderer == nullptr || + (!bPointersIntact && m_pAdapter != nullptr)) { + return false; + } + + if(m_pMeshLayer == nullptr) { + if(pScene->getLayer( + QString::fromLatin1(RESULT_MESH_LAYER_ID)) != nullptr) { + return false; + } + nmVTKMeshLayer* pLayer = new nmVTKMeshLayer( + QString::fromLatin1(RESULT_MESH_LAYER_ID)); + pLayer->setVisible(false); + if(!pScene->addLayer(pLayer)) { + delete pLayer; + return false; + } + m_pMeshLayer = pLayer; + bCreatedMesh = true; + m_pMeshLayer->getActor()->GetProperty()->SetEdgeColor( + 0.0, 0.0, 0.0); + m_pMeshLayer->getActor()->GetProperty()->LightingOff(); + } + + if(m_pScalarBarLayer == nullptr) { + if(pScene->getLayer(QString::fromLatin1( + RESULT_SCALAR_BAR_LAYER_ID)) != nullptr) { + rollbackCreatedLayers( + bCreatedMesh, false, false); + return false; + } + nmVTKScalarBarLayer* pLayer = new nmVTKScalarBarLayer( + QString::fromLatin1(RESULT_SCALAR_BAR_LAYER_ID)); + pLayer->setVisible(false); + pLayer->setPosition(0.05, 0.1); + pLayer->setSize(0.1, 0.8); + if(!pScene->addLayer(pLayer)) { + delete pLayer; + rollbackCreatedLayers( + bCreatedMesh, false, false); + return false; + } + m_pScalarBarLayer = pLayer; + bCreatedScalarBar = true; + } + + if(m_pScaleGridLayer == nullptr) { + if(pScene->getLayer(QString::fromLatin1( + RESULT_SCALE_GRID_LAYER_ID)) != nullptr) { + rollbackCreatedLayers( + bCreatedMesh, bCreatedScalarBar, false); + return false; + } + nmVTKPlanarScaleGridLayer* pLayer = + new nmVTKPlanarScaleGridLayer( + QString::fromLatin1(RESULT_SCALE_GRID_LAYER_ID)); + pLayer->setVisible(false); + if(!pScene->addLayer(pLayer)) { + delete pLayer; + rollbackCreatedLayers( + bCreatedMesh, bCreatedScalarBar, false); + return false; + } + m_pScaleGridLayer = pLayer; + bCreatedScaleGrid = true; + } + return true; + } + + void rollbackCreatedLayers(bool bCreatedMesh, + bool bCreatedScalarBar, + bool bCreatedScaleGrid) + { + synchronizeLayerPointers(); + nmVTKScene* pScene = getScene(); + if(pScene == nullptr) { + return; + } + if(bCreatedScaleGrid && m_pScaleGridLayer != nullptr && + pScene->getLayer( + QString::fromLatin1(RESULT_SCALE_GRID_LAYER_ID)) == + m_pScaleGridLayer) { + pScene->removeLayer( + QString::fromLatin1(RESULT_SCALE_GRID_LAYER_ID)); + } + if(bCreatedScaleGrid) { + m_pScaleGridLayer = nullptr; + } + if(bCreatedScalarBar && m_pScalarBarLayer != nullptr && + pScene->getLayer( + QString::fromLatin1(RESULT_SCALAR_BAR_LAYER_ID)) == + m_pScalarBarLayer) { + pScene->removeLayer( + QString::fromLatin1(RESULT_SCALAR_BAR_LAYER_ID)); + } + if(bCreatedScalarBar) { + m_pScalarBarLayer = nullptr; + } + if(bCreatedMesh && m_pMeshLayer != nullptr && + pScene->getLayer(QString::fromLatin1(RESULT_MESH_LAYER_ID)) == + m_pMeshLayer) { + pScene->removeLayer( + QString::fromLatin1(RESULT_MESH_LAYER_ID)); + } + if(bCreatedMesh) { + m_pMeshLayer = nullptr; + } + } + + bool bindMeshAdapter(nmPebiResultSnapshotRenderAdapter* pAdapter, + const double dDisplayRange[2]) + { + if(m_pMeshLayer == nullptr || pAdapter == nullptr || + dDisplayRange == nullptr) { + return false; + } + + // MeshLayer 在触碰旧 Mapper 前完成全部校验,失败不会破坏旧画面。 + return m_pMeshLayer->setInputConnection( + pAdapter->getOutputPort(), + pAdapter->getCurrentScalarArrayName(), + nmVTKMeshLayer::ScalarAssociation_CellData, + dDisplayRange[0], dDisplayRange[1]); + } + + void applyLayerVisibility(bool bHasData) + { + synchronizeLayerPointers(); + if(m_pMeshLayer != nullptr) { + m_pMeshLayer->setDisplayMode(m_bCellEdgesVisible + ? nmVTKMeshLayer::DisplayMode_SurfaceWithEdges + : nmVTKMeshLayer::DisplayMode_Surface); + m_pMeshLayer->setScalarVisibility(bHasData); + m_pMeshLayer->setVisible(bHasData && m_bMeshVisible); + } + if(m_pScalarBarLayer != nullptr) { + m_pScalarBarLayer->setVisible( + bHasData && m_bScalarBarVisible); + } + if(m_pScaleGridLayer != nullptr) { + m_pScaleGridLayer->setVisible( + bHasData && m_bScaleGridVisible); + } + if(m_pContainer != nullptr) { + m_pContainer->setRotationCenterPickTarget( + bHasData && m_bMeshVisible && m_pMeshLayer != nullptr + ? m_pMeshLayer->getActor() : nullptr); + } + } + + bool restorePreviousScene() + { + synchronizeLayerPointers(); + if(!hasOwnedBaseLayers() || m_pContainer == nullptr) { + return false; + } + if(m_pAdapter == nullptr || m_pSnapshot.isNull()) { + m_pMeshLayer->clearInput(); + m_pContainer->clearPrimaryViewBounds(); + if(m_pScalarBarLayer != nullptr) { + m_pScalarBarLayer->setLookupTable(nullptr); + m_pScalarBarLayer->setTitle(QString()); + } + applyLayerVisibility(false); + return true; + } + + const bool bMeshRestored = bindMeshAdapter( + m_pAdapter, m_dDisplayRange); + bool bGridRestored = !m_bHasBounds; + bool bBoundsRestored = !m_bHasBounds; + if(m_bHasBounds && m_pScaleGridLayer != nullptr) { + bGridRestored = m_pScaleGridLayer->setBounds( + m_dBounds[0], m_dBounds[1], + m_dBounds[2], m_dBounds[3], m_dBounds[4]); + bBoundsRestored = m_pContainer->setPrimaryViewBounds(m_dBounds); + } + if(m_pScalarBarLayer != nullptr) { + m_pScalarBarLayer->setLookupTable( + m_pMeshLayer->getLookupTable()); + } + applyLayerVisibility(true); + return bMeshRestored && bGridRestored && bBoundsRestored; + } + + void releaseActiveData() + { + // Scene 可能已由外部清空;先按稳定 ID 解除所有悬空观察指针。 + synchronizeLayerPointers(); + if(m_pContainer != nullptr) { + m_pContainer->setRotationCenterPickTarget(nullptr); + m_pContainer->clearPrimaryViewBounds(); + } + if(m_pMeshLayer != nullptr) { + m_pMeshLayer->setScalarVisibility(false); + m_pMeshLayer->clearInput(); + m_pMeshLayer->setVisible(false); + } + if(m_pScalarBarLayer != nullptr) { + m_pScalarBarLayer->setVisible(false); + m_pScalarBarLayer->setLookupTable(nullptr); + m_pScalarBarLayer->setTitle(QString()); + } + if(m_pScaleGridLayer != nullptr) { + m_pScaleGridLayer->setVisible(false); + } + delete m_pAdapter; + m_pAdapter = nullptr; + m_pSnapshot.clear(); + m_listAvailablePropertyIds.clear(); + m_sCurrentPropertyId.clear(); + m_nCurrentFrameIndex = -1; + m_bHasPropertyRange = false; + m_bHasBounds = false; + } + + void removeOwnedLayers() + { + synchronizeLayerPointers(); + nmVTKScene* pScene = getScene(); + if(pScene == nullptr) { + return; + } + + // 只删除仍与本控制器记录身份一致的图层,绝不误删同 ID 的外部替代层。 + if(m_pScaleGridLayer != nullptr && + pScene->getLayer( + QString::fromLatin1(RESULT_SCALE_GRID_LAYER_ID)) == + m_pScaleGridLayer) { + pScene->removeLayer( + QString::fromLatin1(RESULT_SCALE_GRID_LAYER_ID)); + } + m_pScaleGridLayer = nullptr; + if(m_pScalarBarLayer != nullptr && + pScene->getLayer( + QString::fromLatin1(RESULT_SCALAR_BAR_LAYER_ID)) == + m_pScalarBarLayer) { + pScene->removeLayer( + QString::fromLatin1(RESULT_SCALAR_BAR_LAYER_ID)); + } + m_pScalarBarLayer = nullptr; + if(m_pMeshLayer != nullptr && + pScene->getLayer(QString::fromLatin1(RESULT_MESH_LAYER_ID)) == + m_pMeshLayer) { + pScene->removeLayer( + QString::fromLatin1(RESULT_MESH_LAYER_ID)); + } + m_pMeshLayer = nullptr; + } + + nmWxVTKRenderContainerWidget* m_pContainer; ///< 非所有权指针。 + nmVTKMeshLayer* m_pMeshLayer; ///< Scene 所有。 + nmVTKScalarBarLayer* m_pScalarBarLayer; ///< Scene 所有。 + nmVTKPlanarScaleGridLayer* m_pScaleGridLayer; ///< Scene 所有。 + nmPebiResultSnapshotRenderAdapter* m_pAdapter; ///< 控制器独占。 + QSharedPointer m_pSnapshot; + QStringList m_listAvailablePropertyIds; + QString m_sCurrentPropertyId; + int m_nCurrentFrameIndex; + double m_dBusinessRange[2]; + double m_dDisplayRange[2]; + double m_dBounds[6]; + bool m_bHasPropertyRange; + bool m_bHasBounds; + bool m_bMeshVisible; + bool m_bScalarBarVisible; + bool m_bScaleGridVisible; + bool m_bCellEdgesVisible; +}; + +nmPebiResultSceneController::nmPebiResultSceneController( + nmWxVTKRenderContainerWidget* pContainer) + : m_pPrivate(new nmPebiResultSceneControllerPrivate(pContainer)) +{ + Q_ASSERT(isResultSceneGuiThread()); +} + +nmPebiResultSceneController::~nmPebiResultSceneController() +{ + Q_ASSERT(isResultSceneGuiThread()); + if(m_pPrivate != nullptr) { + m_pPrivate->releaseActiveData(); + // Controller 结束后立即移除自己的业务图层,使同一容器可安全创建新实例。 + m_pPrivate->removeOwnedLayers(); + m_pPrivate->m_pContainer = nullptr; + delete m_pPrivate; + m_pPrivate = nullptr; + } +} + +bool nmPebiResultSceneController::setSnapshot( + const QSharedPointer& pSnapshot, + QString* pError) +{ + Q_ASSERT(isResultSceneGuiThread()); + if(pError != nullptr) { + pError->clear(); + } + if(!isResultSceneGuiThread() || m_pPrivate == nullptr || + m_pPrivate->m_pContainer == nullptr || pSnapshot.isNull() || + !pSnapshot->isComplete()) { + if(pError != nullptr) { + *pError = controllerText("The result snapshot is invalid."); + } + return false; + } + m_pPrivate->discardActiveDataIfLayersLost(); + if(m_pPrivate->m_pSnapshot == pSnapshot && + m_pPrivate->m_pSnapshot->getSnapshotId() == + pSnapshot->getSnapshotId()) { + return true; + } + + const QString sInitialProperty = chooseInitialProperty(pSnapshot); + nmPebiResultPropertyDescriptor oInitialDescriptor; + if(sInitialProperty.isEmpty() || + !nmPebiResultPropertyCatalog::findDescriptor( + sInitialProperty, oInitialDescriptor) || + oInitialDescriptor.m_eAssociation != + NM_PebiResultPropertyAssociation_CellData) { + if(pError != nullptr) { + *pError = controllerText( + "The result snapshot has no renderable property."); + } + return false; + } + + nmPebiResultSnapshotRenderAdapter* pCandidateAdapter = + new nmPebiResultSnapshotRenderAdapter(); + if(!pCandidateAdapter->initialize( + pSnapshot, + nmPebiResultSnapshotRenderAdapter::SharedPropertyArray) || + !pCandidateAdapter->setPropertyFrame(sInitialProperty, 0)) { + delete pCandidateAdapter; + if(pError != nullptr) { + *pError = controllerText( + "Unable to initialize the result render adapter."); + } + return false; + } + + double dBusinessRange[2]; + double dDisplayRange[2]; + double dBounds[6]; + if(pCandidateAdapter->getCurrentScalarArrayName().isEmpty() || + !getValidatedPropertyRanges(pSnapshot, sInitialProperty, + dBusinessRange, dDisplayRange) || + !validateAdapterOutput(pCandidateAdapter, + pSnapshot->getResultCellCount(), dBounds) || + !areSnapshotWellsValid(pSnapshot)) { + delete pCandidateAdapter; + if(pError != nullptr) { + *pError = controllerText( + "The result render data failed validation."); + } + return false; + } + + bool bCreatedMesh = false; + bool bCreatedScalarBar = false; + bool bCreatedScaleGrid = false; + if(!m_pPrivate->ensureBaseLayers( + bCreatedMesh, bCreatedScalarBar, bCreatedScaleGrid)) { + delete pCandidateAdapter; + if(pError != nullptr) { + *pError = controllerText( + "Unable to create the result render layers."); + } + return false; + } + + bool bCommitted = m_pPrivate->m_pScaleGridLayer->setBounds( + dBounds[0], dBounds[1], dBounds[2], + dBounds[3], dBounds[4]) && + m_pPrivate->m_pContainer->setPrimaryViewBounds(dBounds) && + m_pPrivate->bindMeshAdapter( + pCandidateAdapter, dDisplayRange); + if(!bCommitted) { + const bool bRestored = m_pPrivate->restorePreviousScene(); + if(!bRestored) { + // 不允许 Release 构建留下“逻辑有快照、实际无输入”的半恢复状态。 + m_pPrivate->releaseActiveData(); + } + m_pPrivate->rollbackCreatedLayers( + bCreatedMesh, bCreatedScalarBar, bCreatedScaleGrid); + delete pCandidateAdapter; + if(pError != nullptr) { + *pError = controllerText( + "Unable to commit the result render data."); + } + return false; + } + + nmPebiResultSnapshotRenderAdapter* pPreviousAdapter = + m_pPrivate->m_pAdapter; + m_pPrivate->m_pAdapter = pCandidateAdapter; + m_pPrivate->m_pSnapshot = pSnapshot; + m_pPrivate->m_listAvailablePropertyIds = + pSnapshot->getAvailablePropertyIds(); + m_pPrivate->m_sCurrentPropertyId = sInitialProperty; + m_pPrivate->m_nCurrentFrameIndex = 0; + m_pPrivate->m_dBusinessRange[0] = dBusinessRange[0]; + m_pPrivate->m_dBusinessRange[1] = dBusinessRange[1]; + m_pPrivate->m_dDisplayRange[0] = dDisplayRange[0]; + m_pPrivate->m_dDisplayRange[1] = dDisplayRange[1]; + copyBounds(dBounds, m_pPrivate->m_dBounds); + m_pPrivate->m_bHasPropertyRange = true; + m_pPrivate->m_bHasBounds = true; + m_pPrivate->m_pScalarBarLayer->setLookupTable( + m_pPrivate->m_pMeshLayer->getLookupTable()); + m_pPrivate->applyLayerVisibility(true); + + // Mapper 已不再引用旧 Producer,此时才允许释放旧 Adapter。 + delete pPreviousAdapter; + + const bool bViewFitted = + m_pPrivate->m_pContainer->fitTopViewWithoutRender(); + const bool bCenterReset = + m_pPrivate->m_pContainer->resetRotationCenterWithoutRender(); + Q_ASSERT(bViewFitted && bCenterReset); + Q_UNUSED(bViewFitted); + Q_UNUSED(bCenterReset); + // showReadyState 是本提交路径唯一的 Render 入口。 + m_pPrivate->m_pContainer->showReadyState(); + return true; +} + +void nmPebiResultSceneController::clear() +{ + Q_ASSERT(isResultSceneGuiThread()); + if(isResultSceneGuiThread() && m_pPrivate != nullptr) { + m_pPrivate->releaseActiveData(); + } +} + +bool nmPebiResultSceneController::applyPropertyFrame( + const QString& sPropertyId, + int nFrameIndex, + QString* pError) +{ + Q_ASSERT(isResultSceneGuiThread()); + if(pError != nullptr) { + pError->clear(); + } + if(isResultSceneGuiThread() && m_pPrivate != nullptr) { + m_pPrivate->discardActiveDataIfLayersLost(); + } + if(!isResultSceneGuiThread() || m_pPrivate == nullptr || + m_pPrivate->m_pAdapter == nullptr || + m_pPrivate->m_pSnapshot.isNull() || + !m_pPrivate->hasOwnedBaseLayers() || + !m_pPrivate->m_pSnapshot->hasProperty(sPropertyId) || + nFrameIndex < 0 || + nFrameIndex >= m_pPrivate->m_pSnapshot->getPropertyFrameCount( + sPropertyId)) { + if(pError != nullptr) { + *pError = controllerText( + "The requested result property or frame is invalid."); + } + return false; + } + + nmPebiResultPropertyDescriptor oDescriptor; + if(!nmPebiResultPropertyCatalog::findDescriptor( + sPropertyId, oDescriptor) || + oDescriptor.m_eAssociation != + NM_PebiResultPropertyAssociation_CellData || + (oDescriptor.m_eTimeType == NM_PebiResultProperty_Static && + nFrameIndex != 0)) { + if(pError != nullptr) { + *pError = controllerText( + "The requested result property or frame is invalid."); + } + return false; + } + if(sPropertyId == m_pPrivate->m_sCurrentPropertyId && + nFrameIndex == m_pPrivate->m_nCurrentFrameIndex) { + return true; + } + + double dBusinessRange[2]; + double dDisplayRange[2]; + if(!getValidatedPropertyRanges( + m_pPrivate->m_pSnapshot, sPropertyId, + dBusinessRange, dDisplayRange)) { + if(pError != nullptr) { + *pError = controllerText( + "The result property range is invalid."); + } + return false; + } + + const QString sPreviousProperty = + m_pPrivate->m_sCurrentPropertyId; + const int nPreviousFrame = m_pPrivate->m_nCurrentFrameIndex; + const double dPreviousDisplayRange[2] = { + m_pPrivate->m_dDisplayRange[0], + m_pPrivate->m_dDisplayRange[1] + }; + bool bApplied = m_pPrivate->m_pAdapter->setPropertyFrame( + sPropertyId, nFrameIndex); + double dCurrentBounds[6]; + if(bApplied) { + bApplied = validateAdapterOutput( + m_pPrivate->m_pAdapter, + m_pPrivate->m_pSnapshot->getResultCellCount(), + dCurrentBounds) && + m_pPrivate->m_pMeshLayer->setScalarArray( + m_pPrivate->m_pAdapter->getCurrentScalarArrayName(), + nmVTKMeshLayer::ScalarAssociation_CellData, + dDisplayRange[0], dDisplayRange[1]); + } + if(!bApplied) { + const bool bAdapterRestored = + m_pPrivate->m_pAdapter->setPropertyFrame( + sPreviousProperty, nPreviousFrame); + const bool bMapperRestored = bAdapterRestored && + m_pPrivate->m_pMeshLayer->setScalarArray( + m_pPrivate->m_pAdapter->getCurrentScalarArrayName(), + nmVTKMeshLayer::ScalarAssociation_CellData, + dPreviousDisplayRange[0], dPreviousDisplayRange[1]); + if(!bMapperRestored) { + // 极端恢复失败时清空逻辑和 VTK 状态,保证查询结果与实际画面一致。 + m_pPrivate->releaseActiveData(); + } + if(pError != nullptr) { + *pError = controllerText( + "Unable to apply the result property or frame."); + } + return false; + } + + m_pPrivate->m_sCurrentPropertyId = sPropertyId; + m_pPrivate->m_nCurrentFrameIndex = nFrameIndex; + m_pPrivate->m_dBusinessRange[0] = dBusinessRange[0]; + m_pPrivate->m_dBusinessRange[1] = dBusinessRange[1]; + m_pPrivate->m_dDisplayRange[0] = dDisplayRange[0]; + m_pPrivate->m_dDisplayRange[1] = dDisplayRange[1]; + m_pPrivate->m_bHasPropertyRange = true; + m_pPrivate->m_pContainer->renderView(); + return true; +} + +void nmPebiResultSceneController::setMeshVisible(bool bVisible) +{ + Q_ASSERT(isResultSceneGuiThread()); + if(!isResultSceneGuiThread() || m_pPrivate == nullptr) { + return; + } + m_pPrivate->discardActiveDataIfLayersLost(); + if(m_pPrivate->m_bMeshVisible == bVisible) { + return; + } + m_pPrivate->m_bMeshVisible = bVisible; + const bool bHasData = hasSnapshot(); + m_pPrivate->applyLayerVisibility(bHasData); + if(bHasData) { + m_pPrivate->m_pContainer->renderView(); + } +} + +void nmPebiResultSceneController::setScalarBarVisible(bool bVisible) +{ + Q_ASSERT(isResultSceneGuiThread()); + if(!isResultSceneGuiThread() || m_pPrivate == nullptr) { + return; + } + m_pPrivate->discardActiveDataIfLayersLost(); + if(m_pPrivate->m_bScalarBarVisible == bVisible) { + return; + } + m_pPrivate->m_bScalarBarVisible = bVisible; + const bool bHasData = hasSnapshot(); + m_pPrivate->applyLayerVisibility(bHasData); + if(bHasData) { + m_pPrivate->m_pContainer->renderView(); + } +} + +void nmPebiResultSceneController::setScaleGridVisible(bool bVisible) +{ + Q_ASSERT(isResultSceneGuiThread()); + if(!isResultSceneGuiThread() || m_pPrivate == nullptr) { + return; + } + m_pPrivate->discardActiveDataIfLayersLost(); + if(m_pPrivate->m_bScaleGridVisible == bVisible) { + return; + } + m_pPrivate->m_bScaleGridVisible = bVisible; + const bool bHasData = hasSnapshot(); + m_pPrivate->applyLayerVisibility(bHasData); + if(bHasData) { + m_pPrivate->m_pContainer->renderView(); + } +} + +void nmPebiResultSceneController::setCellEdgesVisible(bool bVisible) +{ + Q_ASSERT(isResultSceneGuiThread()); + if(!isResultSceneGuiThread() || m_pPrivate == nullptr) { + return; + } + m_pPrivate->discardActiveDataIfLayersLost(); + if(m_pPrivate->m_bCellEdgesVisible == bVisible) { + return; + } + m_pPrivate->m_bCellEdgesVisible = bVisible; + const bool bHasData = hasSnapshot(); + m_pPrivate->applyLayerVisibility(bHasData); + if(bHasData) { + m_pPrivate->m_pContainer->renderView(); + } +} + +bool nmPebiResultSceneController::hasSnapshot() const +{ + return isResultSceneGuiThread() && + m_pPrivate != nullptr && + m_pPrivate->m_pAdapter != nullptr && + !m_pPrivate->m_pSnapshot.isNull() && + m_pPrivate->hasOwnedBaseLayers(); +} + +QString nmPebiResultSceneController::getCurrentSnapshotId() const +{ + return hasSnapshot() + ? m_pPrivate->m_pSnapshot->getSnapshotId() : QString(); +} + +QStringList nmPebiResultSceneController::getAvailablePropertyIds() const +{ + return hasSnapshot() + ? m_pPrivate->m_listAvailablePropertyIds : QStringList(); +} + +QString nmPebiResultSceneController::getCurrentPropertyId() const +{ + return hasSnapshot() + ? m_pPrivate->m_sCurrentPropertyId : QString(); +} + +int nmPebiResultSceneController::getCurrentFrameIndex() const +{ + return hasSnapshot() + ? m_pPrivate->m_nCurrentFrameIndex : -1; +} + +bool nmPebiResultSceneController::getCurrentPropertyRange( + double dRange[2]) const +{ + if(!hasSnapshot() || dRange == nullptr || + !m_pPrivate->m_bHasPropertyRange) { + return false; + } + dRange[0] = m_pPrivate->m_dBusinessRange[0]; + dRange[1] = m_pPrivate->m_dBusinessRange[1]; + return true; +} + +bool nmPebiResultSceneController::isMeshVisible() const +{ + return m_pPrivate != nullptr && m_pPrivate->m_bMeshVisible; +} + +bool nmPebiResultSceneController::isScalarBarVisible() const +{ + return m_pPrivate != nullptr && m_pPrivate->m_bScalarBarVisible; +} + +bool nmPebiResultSceneController::isScaleGridVisible() const +{ + return m_pPrivate != nullptr && m_pPrivate->m_bScaleGridVisible; +} + +bool nmPebiResultSceneController::areCellEdgesVisible() const +{ + return m_pPrivate != nullptr && m_pPrivate->m_bCellEdgesVisible; +}