feat(nmNum): 新增三维单平面切片与交互参数设置

- 支持任意方向平面切片、参数输入及拖动松手更新
- 新增非模态切片设置窗口,支持重置和原模型参照
- 复用结果底部工具栏的渲染模式与属性选择
- 完善切片显隐、模型更换及返回计算结果的状态恢复
- 同步中文翻译
feature/PEBI-Region-Average-Pressure-20261102
lh 2 weeks ago
parent 03ceb5e1b3
commit 21745a3bb0

Binary file not shown.

@ -3349,6 +3349,7 @@ No valid error was returned; keep the last accepted result and reduce the step.<
</context>
<context>
<name>nmWxPebiResultControlBar</name>
<message><source>Solid Color</source><translation>纯色</translation></message>
<message><source>Display</source><translation>显示</translation></message>
<message><source>Point</source><translation>点</translation></message>
<message><source>Wireframe</source><translation>线框</translation></message>
@ -3464,6 +3465,58 @@ No valid error was returned; keep the last accepted result and reduce the step.<
%1</source><translation>结果动画已保存到:
%1</translation></message>
<message><source>Unable to export the result animation.</source><translation>无法导出结果动画。</translation></message>
<message>
<source>Import 3D Model</source>
<translation>导入三维模型</translation>
</message>
<message>
<source>Import 3D Model...</source>
<translation>导入三维模型…</translation>
</message>
<message>
<source>Loading 3D model...</source>
<translation>正在加载三维模型…</translation>
</message>
<message>
<source>Replace 3D Model...</source>
<translation>更换三维模型…</translation>
</message>
<message>
<source>Return to Calculation Result</source>
<translation>返回计算结果</translation>
</message>
<message>
<source>Slice</source>
<translation>切片</translation>
</message>
<message>
<source>Unable to display the 3D model.</source>
<translation>无法显示三维模型。</translation>
</message>
<message>
<source>Unable to return to the latest calculation result: %1</source>
<translation>无法返回最新计算结果:%1</translation>
</message>
<message>
<source>VTK Volume Mesh (*.vtk *.vtu)</source>
<translation>VTK 三维体网格 (*.vtk *.vtu)</translation>
</message>
<message>
<source>Show Slice</source>
<translation>显示切片</translation>
</message>
<message>
<source>Slice Settings...</source>
<translation>切片设置…</translation>
</message>
<message>
<source>The slice plane does not intersect the model.</source>
<translation>切片平面与模型无交集。</translation>
</message>
<message>
<source>Unable to display the slice.</source>
<translation>无法显示截面。</translation>
</message>
</context>
<context>
<name>nmWxPebiGridViewWidget</name>
@ -8968,4 +9021,182 @@ Average pressure in contour: %2 MPa</source>
<translation>表皮系数</translation>
</message>
</context>
<context>
<name>nmVolumeData</name>
<message>
<source>Not enough memory to import the volume mesh.</source>
<translation>内存不足,无法导入三维网格。</translation>
</message>
<message>
<source>Only linear tetrahedron, voxel, hexahedron, wedge and pyramid volume cells are supported.</source>
<translation>当前仅支持线性四面体、体素、六面体、楔形和金字塔体单元。</translation>
</message>
<message>
<source>Select a .vtk or .vtu volume mesh file.</source>
<translation>请选择 .vtk 或 .vtu 三维体网格文件。</translation>
</message>
<message>
<source>Skipped invalid scalar array: %1</source>
<translation>已跳过无效标量属性:%1</translation>
</message>
<message>
<source>Skipped scalar array without finite values: %1</source>
<translation>已跳过没有有限值的标量属性:%1</translation>
</message>
<message>
<source>The file contains no volume mesh.</source>
<translation>文件中没有有效的三维体网格。</translation>
</message>
<message>
<source>The mesh connectivity is invalid.</source>
<translation>网格连接关系无效。</translation>
</message>
<message>
<source>The mesh contains an invalid point index.</source>
<translation>网格包含越界的节点索引。</translation>
</message>
<message>
<source>The mesh contains non-finite coordinates.</source>
<translation>网格包含无效坐标(NaN 或无穷值)。</translation>
</message>
<message>
<source>The mesh contains repeated cell vertices.</source>
<translation>网格单元包含重复顶点。</translation>
</message>
<message>
<source>The mesh must have a finite extent in all three directions.</source>
<translation>三维网格必须在三个方向均具有有效范围。</translation>
</message>
<message>
<source>The temporary directory path cannot be read by VTK.</source>
<translation>VTK 无法读取当前临时目录路径。</translation>
</message>
<message>
<source>The volume mesh file is empty or cannot be opened.</source>
<translation>三维网格文件为空或无法打开。</translation>
</message>
<message>
<source>Unable to create a temporary file for this path.</source>
<translation>无法为该文件路径创建临时文件。</translation>
</message>
<message>
<source>Unable to read the volume mesh file.</source>
<translation>无法读取三维网格文件。</translation>
</message>
<message>
<source>VTK could not read the volume mesh: %1</source>
<translation>VTK 无法读取三维网格:%1</translation>
</message>
<message>
<source>Volume import canceled.</source>
<translation>已取消三维模型导入。</translation>
</message>
</context>
<context>
<name>nmVolumeSceneController</name>
<message>
<source>Not enough memory to display the volume mesh.</source>
<translation>内存不足,无法显示三维网格。</translation>
</message>
<message>
<source>Unable to bind the volume scalar property.</source>
<translation>无法绑定三维模型的标量属性。</translation>
</message>
</context>
<context>
<name>nmVolumeImportTask</name>
<message>
<source>An unexpected error occurred while importing the volume mesh.</source>
<translation>导入三维网格时发生意外错误。</translation>
</message>
</context>
<context>
<name>nmVolumeSliceSettings</name>
<message>
<source>Origin and normal must contain finite numbers.</source>
<translation>原点和法向必须为有限数值。</translation>
</message>
<message>
<source>The plane normal must not be zero.</source>
<translation>平面法向不能为零向量。</translation>
</message>
</context>
<context>
<name>nmVolumeSlicePipeline</name>
<message>
<source>No volume model is available.</source>
<translation>当前没有可用的三维模型。</translation>
</message>
<message>
<source>Unable to compute a valid slice. The previous view has been kept.</source>
<translation>无法生成有效截面,已保留原画面。</translation>
</message>
<message>
<source>Not enough memory to compute the slice.</source>
<translation>内存不足,无法计算截面。</translation>
</message>
</context>
<context>
<name>nmWxVolumeSliceSettingsDlg</name>
<message>
<source>Origin %1</source>
<translation>原点 %1</translation>
</message>
<message>
<source>Normal %1</source>
<translation>法向 %1</translation>
</message>
<message>
<source>Plane Parameters</source>
<translation>平面参数</translation>
</message>
<message>
<source>Display Options</source>
<translation>显示选项</translation>
</message>
<message>
<source>Slice Settings</source>
<translation>切片设置</translation>
</message>
<message>
<source>Origin</source>
<translation>原点</translation>
</message>
<message>
<source>Normal</source>
<translation>法向</translation>
</message>
<message>
<source>Model Center</source>
<translation>模型中心</translation>
</message>
<message>
<source>Show Plane</source>
<translation>显示平面</translation>
</message>
<message>
<source>Show Original Model</source>
<translation>显示原模型</translation>
</message>
<message>
<source>Apply</source>
<translation>应用</translation>
</message>
<message>
<source>Reset</source>
<translation>重置</translation>
</message>
<message>
<source>Close</source>
<translation>关闭</translation>
</message>
<message>
<source>Enter valid numbers for origin and normal.</source>
<translation>请为原点和法向输入有效数值。</translation>
</message>
<message>
<source>Drag the center or outline to move the plane; drag arrows to rotate.</source>
<translation>拖动中心手柄或平面轮廓移动平面,拖动箭头旋转平面。</translation>
</message>
</context>
</TS>

@ -3,6 +3,8 @@
#include "nmSubWxs_global.h"
#include "nmVolumeData.h"
#include "nmVolumeSliceSettings.h"
#include "nmVTKMeshLayer.h"
class nmWxVTKRenderContainerWidget;
class nmVolumeSceneControllerPrivate;
@ -30,18 +32,34 @@ public:
bool hasData() const;
/** @brief 按稳定 ID 原子切换着色属性;失败保留原属性,成功至多 Render 一次。 */
bool applyProperty(const QString& sPropertyId, QString* pError = nullptr);
/** @brief 复用结果窗口的网格显示模式,立即生效,不重算截面或移动相机。 */
void setMeshDisplayMode(nmVTKMeshLayer::DisplayMode eMode);
/** @brief 返回完整模型与截面共用的显示模式,供底部工具栏同步。 */
nmVTKMeshLayer::DisplayMode getMeshDisplayMode() const;
/** @brief 更新色标响应式布局,不触发渲染,供容器 Resize 通知使用。 */
void setViewportSize(int nWidth, int nHeight);
/** @brief 保存色标显隐偏好;无标量时始终隐藏,重复设置不渲染。 */
void setScalarBarVisible(bool bVisible);
/** @brief 保存三维坐标框显隐偏好;重复设置不渲染。 */
void setScaleGridVisible(bool bVisible);
/** @brief 返回色标显隐偏好,供 QAction 同步勾选状态。 */
bool isScalarBarVisible() const;
/** @brief 返回三维坐标框显隐偏好,供 QAction 同步勾选状态。 */
bool isScaleGridVisible() const;
/** @brief 返回当前模型是否选中了可用于着色的合法属性。 */
bool hasScalarProperty() const;
/** @brief 返回当前只读数据,用于参数目录及平面范围初始化。 */
QSharedPointer<const nmVolumeData> getData() const;
/** @brief 返回最后成功应用的切片参数;首次切片前返回模型中心默认参数。 */
nmVolumeSliceSettings getSliceSettings() const;
/** @brief 启用或停用切片;首次启用计算中心截面,后续复用已有截面。 */
bool setSliceEnabled(bool bEnabled, QString* pError = nullptr);
/** @brief 事务式应用参数,成功后启用切片;失败保留旧画面及参数。 */
bool applySliceSettings(const nmVolumeSliceSettings& oSettings, QString* pError = nullptr);
/** @brief 立即更新辅助平面及参照模型偏好,不执行切片计算。 */
void setSliceDisplayOptions(bool bShowPlane, bool bShowSource);
/** @brief 返回当前是否显示截面,供菜单同步勾选状态。 */
bool isSliceEnabled() const;
/** @brief 返回当前切片是否为合法的空截面,供界面显示无交集提示。 */
bool isSliceEmpty() const;
/** @brief 返回当前数据成功提交截面的次数,便于观察及验证预览不重算。 */
unsigned int getSliceRevision() const;
private:
Q_DISABLE_COPY(nmVolumeSceneController)

@ -0,0 +1,62 @@
#ifndef NMVOLUMESLICEINTERACTION_H
#define NMVOLUMESLICEINTERACTION_H
#include "nmSubWxs_global.h"
#include "nmVolumeSliceSettings.h"
#include <QObject>
#include <vtkSmartPointer.h>
class nmWxVTKRenderContainerWidget;
class vtkObject;
class vtkCallbackCommand;
class vtkImplicitPlaneWidget2;
class vtkImplicitPlaneRepresentation;
/** @brief 管理预览平面及 VTK/Qt 事件边界;不持有 Cutter,任何预览均不能触发切片计算。 */
class NM_SUB_WXS_EXPORT nmVolumeSliceInteraction : public QObject
{
Q_OBJECT
public:
/** @brief 绑定生命周期更长的渲染容器;构造时不显示 Widget。 */
explicit nmVolumeSliceInteraction(nmWxVTKRenderContainerWidget* pContainer, QObject* pParent = nullptr);
/** @brief 先禁用 Widget、解除观察者,再释放 VTK 引用。 */
virtual ~nmVolumeSliceInteraction();
/** @brief 为当前数据身份放置预览平面;同一数据的输入更新使用 setPreview。 */
void bind(const QString& sDataId, const double dBounds[6], const nmVolumeSliceSettings& oSettings);
/** @brief 解除数据及 Interactor,并使尚未处理的拖动事件失效。 */
void unbind();
/** @brief 更新有效草稿的位置及方向,非法参数不改变预览。 */
void setPreview(const nmVolumeSliceSettings& oSettings);
/** @brief 控制辅助平面及事件处理;关闭时丢弃排队事件,保持绑定参数。 */
void setEnabled(bool bEnabled);
/** @brief 返回辅助平面的实际启用状态,供测试与界面核对。 */
bool isEnabled() const;
signals:
/** @brief 队列合并后的拖动预览,仅同步参数,不应计算截面。 */
void previewChanged(const QString& sDataId, const nmVolumeSliceSettings& oSettings);
/** @brief 松手后发出一次应用请求;接收者还需核对当前数据身份和模式。 */
void applyRequested(const QString& sDataId, const nmVolumeSliceSettings& oSettings);
private slots:
/** @brief 离开 VTK 回调栈后交付最后一次预览及可选应用请求。 */
void deliverPendingEvent();
private:
Q_DISABLE_COPY(nmVolumeSliceInteraction)
/** @brief VTK 回调只复制数据、安排 Qt 队列,不执行界面槽或切片计算。 */
static void onPlaneEvent(vtkObject* pCaller, unsigned long nEvent, void* pClientData, void* pCallData);
nmWxVTKRenderContainerWidget* m_pContainer; ///< 非所有权,必须在此对象之后销毁。
vtkSmartPointer<vtkImplicitPlaneRepresentation> m_pRepresentation; ///< 独立的预览几何,不与计算平面共享。
vtkSmartPointer<vtkImplicitPlaneWidget2> m_pWidget; ///< 本窗口独占的 VTK 交互 Widget。
vtkSmartPointer<vtkCallbackCommand> m_pCallback; ///< ClientData 指向本对象,析构前解除。
QString m_sDataId; ///< 当前模型身份,防止旧模型拖动事件修改新模型。
nmVolumeSliceSettings m_oPreview; ///< 最近一次有效草稿及显示选项。
nmVolumeSliceSettings m_oPending; ///< 回调复制的最新平面参数。
unsigned int m_nGeneration; ///< 绑定或关闭交互时递增,隔离同一模型的过期事件。
unsigned int m_nPendingGeneration; ///< 排队事件所属交互代次。
bool m_bQueued; ///< 是否已有待执行 Qt 队列槽,避免每次 MouseMove 排队。
bool m_bPendingApply; ///< 最新事件序列是否包含松手应用。
};
#endif // NMVOLUMESLICEINTERACTION_H

@ -0,0 +1,25 @@
#ifndef NMVOLUMESLICEPIPELINE_H
#define NMVOLUMESLICEPIPELINE_H
#include "nmSubWxs_global.h"
#include "nmVolumeData.h"
#include "nmVolumeSliceSettings.h"
class vtkPolyData;
/** @brief 单次切片计算入口,无窗口依赖;每次计算独占 Plane/Cutter,不连接预览平面。 */
class NM_SUB_WXS_EXPORT nmVolumeSlicePipeline
{
public:
/**
* @brief 从已发布体网格生成独立截面,不修改原始几何或属性数组。
* @param pOutput 仅成功时替换;零单元输出代表合法的无交集结果。
* @return 参数、VTK 计算或输出校验失败返回 false,并保持原输出引用。
*/
static bool build(const QSharedPointer<const nmVolumeData>& pData,
const nmVolumeSliceSettings& oSettings,
vtkSmartPointer<vtkPolyData>& pOutput,
QString* pError = nullptr);
};
#endif // NMVOLUMESLICEPIPELINE_H

@ -0,0 +1,29 @@
#ifndef NMVOLUMESLICESETTINGS_H
#define NMVOLUMESLICESETTINGS_H
#include "nmSubWxs_global.h"
#include <QMetaType>
#include <QString>
class nmVolumeData;
/** @brief 单平面切片的值类型参数;不持有 VTK 对象,可安全复制为界面草稿。 */
struct NM_SUB_WXS_EXPORT nmVolumeSliceSettings
{
/** @brief 初始化 Z 向平面,默认显示辅助平面、不显示原模型。 */
nmVolumeSliceSettings();
/** @brief 从完整模型边界初始化中心平面,并选取模型的默认着色属性。 */
static nmVolumeSliceSettings fromData(const nmVolumeData& oData);
/** @brief 校验有限原点及非零法向;成功才原位归一化,失败不改变参数。 */
bool normalize(QString* pError = nullptr);
double m_dOrigin[3]; ///< 平面经过的位置,使用数据的原始坐标,不推断单位。
double m_dNormal[3]; ///< 草稿允许任意非零向量,提交后的向量始终为单位向量。
QString m_sPropertyId; ///< 关联类型及数组名组成的属性身份;无属性模型为空。
bool m_bShowPlane; ///< 参数窗口打开时是否显示辅助平面及拖动手柄。
bool m_bShowSource; ///< 切片模式下是否显示灰色半透明的原模型。
};
Q_DECLARE_METATYPE(nmVolumeSliceSettings)
#endif // NMVOLUMESLICESETTINGS_H

@ -15,11 +15,12 @@ class QVariant;
class nmPebiResultPlaybackController;
class nmPebiResultSceneController;
class nmVolumeSceneController;
/**
* @brief 结果属性选择与时间回放底部控制条。
* @brief 结果与外部三维模型共用的显示、属性及时间回放底部控制条。
*
* 控件只镜像 PlaybackController/SceneController 状态,不创建 Renderer、
* 控件只镜像当前数据源的控制器状态;三维模式禁用时间回放,不创建 Renderer、
* Actor、Mapper、Adapter 或任何额外 VTK 管线。
*/
class NM_SUB_WXS_EXPORT nmWxPebiResultControlBar : public QWidget
@ -38,6 +39,8 @@ public:
void stopPlayback();
/** @brief 在外部完成 Scene 快照提交或清理后同步全部控件。 */
void synchronizeFromScene();
/** @brief 绑定外部三维场景以复用显示和属性控件;传空指针恢复计算结果底栏。 */
void setVolumeSceneController(nmVolumeSceneController* pController);
/**
* @brief 返回本控制条独占的回放控制器。
* @note 仅供组合窗口查询或配置,调用方不得保存到控制条生命周期之外。
@ -100,6 +103,8 @@ private:
/** @brief 按固定顺序创建属性、导航、时间、播放和设置控件。 */
void createUi();
/** @brief 镜像三维场景的显示模式和原始属性名,保留但禁用时间、动画及颜色设置。 */
void refreshFromVolumeController();
/** @brief 按快照属性顺序重建属性项,itemData 保存稳定 ID。 */
void rebuildPropertyItems(const QStringList& listPropertyIds);
/** @brief 按小时制时间轴重建时间项,itemData 保存零基帧。 */
@ -114,8 +119,12 @@ private:
/** @brief 本控制条独占的 QObject 子对象,负责全部回放业务状态。 */
nmPebiResultPlaybackController* m_pPlaybackController;
/** @brief 已渲染属性、帧和显示设置的唯一真源;本控件不拥有。 */
/** @brief 计算结果的属性、帧和显示设置来源;三维模式保留但不修改,本控件不拥有。 */
nmPebiResultSceneController* m_pSceneController;
/** @brief 当前外部三维场景,非所有权;宿主必须在释放控制器之前解除绑定。 */
nmVolumeSceneController* m_pVolumeSceneController;
/** @brief 三维属性列表对应的数据身份,更换模型时据此重建条目。 */
QString m_sDisplayedVolumeDataId;
/** @brief 显示模式选择框,条目数据保存 DisplayMode 枚举值。 */
QComboBox* m_pDisplayModeCombo;
/** @brief 属性选择框,条目数据保存稳定英文属性 ID。 */

@ -24,6 +24,7 @@ class iUnitGroup;
class nmDataAnalyzeManager;
class nmWxPebiResultRegionState;
class nmWxPebiVolumeState;
struct nmVolumeSliceSettings;
class nmPebiResultSceneController;
class nmPebiResultSnapshot;
class nmWxPebiResultControlBar;
@ -136,6 +137,26 @@ public:
bool isImportedVolumeMode() const;
private slots:
/** @brief 在完整模型与已应用截面之间切换,首次启用时计算中心截面。 */
void onVolumeSliceToggled(bool bEnabled);
/** @brief 按需打开非模态参数窗口,并绑定当前数据的辅助平面。 */
void onShowVolumeSliceSettings();
/** @brief 校验窗口草稿并事务式提交一次截面。 */
void onApplyVolumeSlice();
/** @brief 将有效参数草稿同步到辅助平面,绝不执行切片计算。 */
void onVolumeSliceDraftChanged();
/** @brief 即时更新辅助平面和原模型的显示偏好。 */
void onVolumeSliceDisplayChanged(bool bShowPlane, bool bShowSource);
/** @brief 隐藏窗口时解绑辅助平面,保留已应用截面。 */
void onVolumeSliceDialogHidden();
/** @brief 接收经过队列合并的拖动预览,检查数据身份后同步输入框。 */
void onVolumeSlicePreview(const QString& sDataId, const nmVolumeSliceSettings& oSettings);
/** @brief 处理松手请求;再次检查当前模型和模式,然后应用当前有效草稿。 */
void onVolumeSliceDragApplied(const QString& sDataId, const nmVolumeSliceSettings& oSettings);
/** @brief 根据窗口、加载任务及旋转中心拾取状态,统一控制平面交互可用性。 */
void synchronizeSliceInteraction();
/** @brief 在当前场景事务结束后的事件循环刷新手柄显隐,避免拾取时残留旧画面。 */
void refreshVolumeSliceView();
/** @brief 从切片菜单选择文件,再启动可取消后台导入。 */
void onImportVolumeRequested();
/** @brief 原子标记取消;不等待或强行终止 Reader。 */
@ -199,6 +220,8 @@ private:
bool canImportVolume() const;
/** @brief 同步切片菜单及外部模式下的业务动作,不触发渲染。 */
void synchronizeVolumeActions();
/** @brief 关闭参数窗口并解绑平面,不渲染;释放数据时同时解除对旧模型的引用。 */
void closeVolumeSliceSettings(bool bReleaseData);
/** @brief 创建区域图层和临时交互样式,并冻结当前 pressure 显示身份。 */
bool startRegionPressureDrawing();
/** @brief 解除临时交互、观察者和控制条锁,不修改已准备的统计区域。 */

@ -0,0 +1,81 @@
#ifndef NMWXVOLUMESLICESETTINGSDLG_H
#define NMWXVOLUMESLICESETTINGSDLG_H
#include "iDlgBase.h"
#include "nmSubWxs_global.h"
#include "nmVolumeData.h"
#include "nmVolumeSliceSettings.h"
class QCheckBox;
class QHideEvent;
class QLabel;
class QLineEdit;
class QPushButton;
/** @brief 复用通用参数对话框基类的非模态切片窗口;草稿与已应用参数分离。 */
class NM_SUB_WXS_EXPORT nmWxVolumeSliceSettingsDlg : public iDlgBase
{
Q_OBJECT
public:
/** @brief 创建原点、法向及显示选项控件,不自动显示窗口;着色属性由底栏控制。 */
explicit nmWxVolumeSliceSettingsDlg(QWidget* pParent = nullptr);
/** @brief 更换当前模型及已应用参数,清除前一模型的草稿和错误。 */
void setData(const QSharedPointer<const nmVolumeData>& pData, const nmVolumeSliceSettings& oApplied);
/** @brief 提交成功后同步归一化参数,同时更新重置目标。 */
void setAppliedSettings(const nmVolumeSliceSettings& oSettings);
/** @brief 同步底栏已应用的着色属性,保留原点和法向的未应用草稿。 */
void setAppliedProperty(const QString& sPropertyId);
/** @brief 从当前输入读取并校验草稿,失败不改变输出。 */
bool getDraft(nmVolumeSliceSettings& oSettings, QString* pError = nullptr) const;
/** @brief 从拖动事件更新原点和法向,不改变底栏已应用的属性,不回发预览信号。 */
void setPlanePreview(const nmVolumeSliceSettings& oSettings);
/** @brief 在窗口内显示计算或绑定错误,不弹出阻塞对话框。 */
void setError(const QString& sError);
signals:
/** @brief 请求应用当前有效草稿,由所属结果窗口负责事务提交。 */
void applyRequested();
/** @brief 有效草稿变化,仅用于同步辅助平面,不能触发截面计算。 */
void previewChanged();
/** @brief 显示选项立即生效,与原点、法向草稿的应用周期分离。 */
void displayOptionsChanged(bool bShowPlane, bool bShowSource);
/** @brief 窗口隐藏后通知宿主关闭交互手柄;截面继续显示。 */
void dialogHidden();
protected:
/** @brief 任何隐藏路径均丢弃未应用草稿,避免重开后显示过期参数。 */
virtual void hideEvent(QHideEvent* pEvent);
private slots:
/** @brief 校验当前输入并更新应用按钮;有效输入更新平面预览。 */
void onDraftChanged();
/** @brief 将平面法向设置为 XY/XZ/YZ 对应轴,保持原点。 */
void onAxisRequested();
/** @brief 将草稿原点移到模型中心,保持法向与属性。 */
void onCenterRequested();
/** @brief 恢复最后已应用参数并同步辅助平面。 */
void onResetRequested();
/** @brief 校验有效后请求宿主提交当前草稿。 */
void onApplyRequested();
/** @brief 更新已应用状态中的显示偏好,不提交原点和法向草稿。 */
void onDisplayOptionsChanged();
private:
/** @brief 按现有参数窗口样式创建分组、对齐的编辑控件及固定顺序的底部按钮。 */
void createUi();
/** @brief 阻止信号递归地把值对象写入所有控件,然后更新校验提示。 */
void writeSettings(const nmVolumeSliceSettings& oSettings);
/** @brief 更新按钮及校验消息,不发送预览或应用信号。 */
void updateValidation();
QSharedPointer<const nmVolumeData> m_pData; ///< 当前只读模型,用于有效性检查及中心位置。
nmVolumeSliceSettings m_oApplied; ///< 最后成功提交的参数,含立即生效的显示偏好。
QLineEdit* m_pOriginEdits[3]; ///< 原点 X/Y/Z 文本输入,支持科学计数法。
QLineEdit* m_pNormalEdits[3]; ///< 法向 X/Y/Z 文本输入,应用时归一化。
QCheckBox* m_pShowPlaneCheck; ///< 参数窗口打开时显示辅助平面的开关。
QCheckBox* m_pShowSourceCheck; ///< 灰色半透明原模型的即时显示开关。
QLabel* m_pErrorLabel; ///< 非模态输入或计算错误提示。
QPushButton* m_pApplyButton; ///< 草稿合法且模型存在时才启用。
bool m_bUpdating; ///< 程序同步控件时屏蔽业务信号,防止反馈循环。
};
#endif // NMWXVOLUMESLICESETTINGSDLG_H

@ -1,7 +1,7 @@
#include "nmVolumeSceneController.h"
#include "nmVolumeDataRenderAdapter.h"
#include "nmVolumeSlicePipeline.h"
#include "nmWxVTKRenderContainerWidget.h"
#include "nmVTKBoundsGridLayer.h"
#include "nmVTKMeshLayer.h"
#include "nmVTKScalarBarLayer.h"
#include "nmVTKScene.h"
@ -13,18 +13,18 @@
#include <cmath>
#include <new>
#include <vtkActor.h>
#include <vtkCamera.h>
#include <vtkCellData.h>
#include <vtkDataArray.h>
#include <vtkDataSetMapper.h>
#include <vtkLookupTable.h>
#include <vtkMath.h>
#include <vtkProperty.h>
#include <vtkRenderer.h>
#include <vtkPointData.h>
#include <vtkPolyData.h>
namespace {
// 固定文本标记供 Qt 4 lupdate 提取;运行期通过统一上下文翻译。
static const char* const s_pVolumeTranslationMarkers[] = {
QT_TRANSLATE_NOOP("nmVolumeSceneController", " (Cell)"),
QT_TRANSLATE_NOOP("nmVolumeSceneController", " (Point)"),
QT_TRANSLATE_NOOP("nmVolumeSceneController", "Not enough memory to display the volume mesh."),
QT_TRANSLATE_NOOP("nmVolumeSceneController", "Unable to bind the volume scalar property.")
};
@ -41,6 +41,48 @@ QString sceneText(const char* pText)
Q_UNUSED(s_pVolumeTranslationMarkers);
return QCoreApplication::translate("nmVolumeSceneController", pText);
}
/** @brief 完整模型与截面共用的全局线性颜色表;常量范围只为渲染作最小扩展。 */
vtkSmartPointer<vtkLookupTable> makeVolumeLookupTable(const nmVolumePropertyDescriptor& oProperty)
{
double dMinimum = oProperty.m_dMinimum;
double dMaximum = oProperty.m_dMaximum;
if(dMinimum == dMaximum) {
const double dPadding = qMax(1.0, std::fabs(dMinimum)) * 1e-6;
dMaximum += dPadding;
if(!vtkMath::IsFinite(dMaximum)) { dMaximum = dMinimum; dMinimum -= dPadding; }
}
vtkSmartPointer<vtkLookupTable> pTable = vtkSmartPointer<vtkLookupTable>::New();
pTable->SetNumberOfTableValues(256);
pTable->SetHueRange(0.67, 0.0);
pTable->SetTableRange(dMinimum, dMaximum);
pTable->SetNanColor(0.65, 0.65, 0.65, 1.0);
pTable->Build();
return pTable;
}
/** @brief 候选挂入 Scene 后的回退保护;异常或提前返回时仅撤销该候选图层。 */
class VolumeSliceLayerGuard
{
public:
/** @brief 在挂载之前准备 ID,避免挂载后字符串分配失败造成图层遗留。 */
VolumeSliceLayerGuard(nmVTKScene* pScene, const QString& sId)
: m_pScene(pScene), m_sId(sId), m_pLayer(nullptr) {}
/** @brief 候选未提交时解除 Scene 所有权并删除,已提交时不操作。 */
~VolumeSliceLayerGuard()
{
if(m_pLayer != nullptr && m_pScene->getLayer(m_sId) == m_pLayer) m_pScene->removeLayer(m_sId);
}
/** @brief 记录成功挂载的图层,由 Scene 实际拥有对象。 */
void hold(nmVTKMeshLayer* pLayer) { m_pLayer = pLayer; }
/** @brief 事务提交后解除回退保护。 */
void release() { m_pLayer = nullptr; }
private:
Q_DISABLE_COPY(VolumeSliceLayerGuard)
nmVTKScene* m_pScene; ///< 非所有权 Scene,存活期覆盖本次同步事务。
QString m_sId; ///< 待提交候选的唯一身份。
nmVTKMeshLayer* m_pLayer; ///< Scene 所有的候选指针,仅用于身份检查。
};
}
class nmVolumeSceneControllerPrivate
@ -49,17 +91,19 @@ public:
/** @brief 初始化未挂载图层的空场景,构造本身不改动画面。 */
explicit nmVolumeSceneControllerPrivate(nmWxVTKRenderContainerWidget* pContainer)
: m_pContainer(pContainer), m_pAdapter(nullptr), m_pMesh(nullptr),
m_pScalarBar(nullptr), m_pBounds(nullptr), m_bVisible(false),
m_bScalarBarVisible(true), m_bScaleGridVisible(false) {}
m_pScalarBar(nullptr), m_pSliceMesh(nullptr),
m_eMeshDisplayMode(nmVTKMeshLayer::DisplayMode_SurfaceWithEdges),
m_bSliceEnabled(false), m_nSliceRevision(0), m_bVisible(false),
m_bScalarBarVisible(true) {}
/** @brief 仅移除仍具有相同对象身份的图层,防止误删外部同 ID 图层。 */
~nmVolumeSceneControllerPrivate()
{
// Scene 拥有图层,先解除 Mapper 对适配器的引用,再释放数据。
nmVTKScene* pScene = m_pContainer == nullptr ? nullptr : m_pContainer->getScene();
if(pScene != nullptr) {
if(m_pSliceMesh != nullptr && pScene->getLayer(m_sSliceId) == m_pSliceMesh) pScene->removeLayer(m_sSliceId);
if(m_pMesh != nullptr && pScene->getLayer(m_sMeshId) == m_pMesh) pScene->removeLayer(m_sMeshId);
if(m_pScalarBar != nullptr && pScene->getLayer(m_sScalarBarId) == m_pScalarBar) pScene->removeLayer(m_sScalarBarId);
if(m_pBounds != nullptr && pScene->getLayer(m_sBoundsId) == m_pBounds) pScene->removeLayer(m_sBoundsId);
}
delete m_pAdapter;
}
@ -68,55 +112,53 @@ public:
{
nmVTKScene* pScene = m_pContainer == nullptr ? nullptr : m_pContainer->getScene();
return !m_pData.isNull() && pScene != nullptr && m_pMesh != nullptr &&
m_pScalarBar != nullptr && m_pBounds != nullptr &&
m_pScalarBar != nullptr &&
pScene->getLayer(m_sMeshId) == m_pMesh &&
pScene->getLayer(m_sScalarBarId) == m_pScalarBar &&
pScene->getLayer(m_sBoundsId) == m_pBounds;
(m_pSliceMesh == nullptr || pScene->getLayer(m_sSliceId) == m_pSliceMesh);
}
/** @brief 将模型可见状态与各覆盖层偏好合成,不触发 Render。 */
void updateVisibility()
{
if(!isIntact()) return;
m_pMesh->setVisible(m_bVisible);
m_pScalarBar->setVisible(m_bVisible && m_bScalarBarVisible && !m_sPropertyId.isEmpty());
m_pBounds->setVisible(m_bVisible && m_bScaleGridVisible);
const bool bHasSlice = m_pSliceData != nullptr && m_pSliceData->GetNumberOfCells() > 0;
// 参照模型固定为灰色半透明表面;完整模型和截面沿用底栏选中的显示模式。
m_pMesh->setOpacity(m_bSliceEnabled ? 0.15 : 1.0);
m_pMesh->setScalarVisibility(!m_bSliceEnabled && !m_sPropertyId.isEmpty());
m_pMesh->setDisplayMode(m_bSliceEnabled
? nmVTKMeshLayer::DisplayMode_Surface : m_eMeshDisplayMode);
m_pMesh->setVisible(m_bVisible && (!m_bSliceEnabled || m_oSliceSettings.m_bShowSource));
if(m_pSliceMesh != nullptr) {
m_pSliceMesh->setDisplayMode(m_eMeshDisplayMode);
m_pSliceMesh->setVisible(m_bVisible && m_bSliceEnabled && bHasSlice);
}
m_pScalarBar->setVisible(m_bVisible && m_bScalarBarVisible && !m_sPropertyId.isEmpty() && (!m_bSliceEnabled || bHasSlice));
}
/** @brief 仅在当前场景激活时更新拾取目标,隐藏候选不会抢占其他业务场景。 */
void updatePickTarget()
{
if(!m_bVisible || !isIntact()) return;
vtkActor* pTarget = m_pMesh->getActor();
if(m_bSliceEnabled) {
pTarget = m_pSliceMesh != nullptr && m_pSliceData != nullptr && m_pSliceData->GetNumberOfCells() > 0
? m_pSliceMesh->getActor() : nullptr;
}
m_pContainer->setRotationCenterPickTarget(pTarget);
}
/** @brief 完整验证候选 LUT 和属性后提交 Mapper,失败时保持旧绑定。 */
bool bindProperty(const QString& sPropertyId)
bool bindProperty(const QString& sPropertyId, vtkLookupTable* pPreparedTable = nullptr)
{
const nmVolumePropertyDescriptor* pProperty = m_pData->findProperty(sPropertyId);
if(pProperty == nullptr) return false;
double dMinimum = pProperty->m_dMinimum;
double dMaximum = pProperty->m_dMaximum;
const bool bConstant = dMinimum == dMaximum;
if(bConstant) {
// LUT 需要严格递增区间;色标仍显示真实常量值,不能伪造业务范围。
const double dPadding = qMax(1.0, std::fabs(dMinimum)) * 1e-6;
dMaximum += dPadding;
if(!vtkMath::IsFinite(dMaximum)) {
dMaximum = dMinimum;
dMinimum -= dPadding;
}
}
vtkSmartPointer<vtkLookupTable> pTable = vtkSmartPointer<vtkLookupTable>::New();
pTable->SetNumberOfTableValues(256);
pTable->SetHueRange(0.67, 0.0);
pTable->SetTableRange(dMinimum, dMaximum);
pTable->SetNanColor(0.65, 0.65, 0.65, 1.0);
pTable->Build();
const bool bConstant = pProperty->m_dMinimum == pProperty->m_dMaximum;
vtkSmartPointer<vtkLookupTable> pTable = pPreparedTable;
if(pTable == nullptr) pTable = makeVolumeLookupTable(*pProperty);
const nmVTKMeshLayer::ScalarAssociation eAssociation =
pProperty->m_eAssociation == NM_VolumeProperty_CellData
? nmVTKMeshLayer::ScalarAssociation_CellData : nmVTKMeshLayer::ScalarAssociation_PointData;
if(!m_pMesh->setInputConnection(m_pAdapter->getOutputPort(), pProperty->m_sArrayName, eAssociation, pTable)) return false;
m_pMesh->setScalarVisibility(true);
QString sTitle = pProperty->m_sArrayName;
const QString sOtherId = (pProperty->m_eAssociation == NM_VolumeProperty_CellData
? QString::fromLatin1("point:") : QString::fromLatin1("cell:")) + sTitle;
if(m_pData->findProperty(sOtherId) != nullptr) {
sTitle += pProperty->m_eAssociation == NM_VolumeProperty_CellData
? sceneText(" (Cell)") : sceneText(" (Point)");
}
m_pScalarBar->setTitle(sTitle);
m_pScalarBar->setTitle(pProperty->m_sArrayName);
m_pScalarBar->setLookupTable(pTable);
m_pScalarBar->setConstantValueLabel(bConstant, QString::number(pProperty->m_dMinimum, 'g', 8));
m_pScalarBar->setNumberOfLabels(bConstant ? 0 : m_pScalarBar->getResponsiveLabelCount());
@ -128,14 +170,18 @@ public:
nmVolumeDataRenderAdapter* m_pAdapter; ///< 本状态独占的渲染管线适配器。
nmVTKMeshLayer* m_pMesh; ///< Scene 所有的主模型图层,仅作非所有权观察。
nmVTKScalarBarLayer* m_pScalarBar; ///< Scene 所有的色标图层。
nmVTKBoundsGridLayer* m_pBounds; ///< Scene 所有的三维坐标框图层。
nmVTKMeshLayer* m_pSliceMesh; ///< Scene 所有的截面图层;每次成功计算后替换。
vtkSmartPointer<vtkPolyData> m_pSliceData; ///< 与 Cutter 断开的独立截面,关闭显示时保留。
nmVolumeSliceSettings m_oSliceSettings; ///< 当前数据最后一次成功应用的切片参数。
nmVTKMeshLayer::DisplayMode m_eMeshDisplayMode; ///< 完整模型及截面的共用显示偏好,不参与切片计算。
QString m_sSliceId; ///< 当前截面图层唯一 ID,不与完整模型图层复用。
bool m_bSliceEnabled; ///< 是否正在显示截面,而非完整模型。
unsigned int m_nSliceRevision; ///< 本次数据成功应用截面的次数,预览不增加。
QString m_sMeshId; ///< 主模型图层稳定 ID,带本次候选唯一前缀。
QString m_sScalarBarId; ///< 色标图层稳定 ID。
QString m_sBoundsId; ///< 三维坐标框图层稳定 ID。
QString m_sPropertyId; ///< 最后成功绑定的属性 ID,纯几何时为空。
bool m_bVisible; ///< 是否已由窗口事务激活,而非尚在准备的隐藏候选。
bool m_bScalarBarVisible; ///< 当前三维模型的独立色标偏好。
bool m_bScaleGridVisible; ///< 当前三维模型的独立坐标框偏好。
};
nmVolumeSceneController::nmVolumeSceneController(nmWxVTKRenderContainerWidget* pContainer)
@ -156,12 +202,12 @@ bool nmVolumeSceneController::setData(const QSharedPointer<const nmVolumeData>&
try {
QScopedPointer<nmVolumeSceneControllerPrivate> pCandidate(new nmVolumeSceneControllerPrivate(m_pPrivate->m_pContainer));
pCandidate->m_pData = pData;
pCandidate->m_oSliceSettings = nmVolumeSliceSettings::fromData(*pData);
pCandidate->m_pAdapter = new nmVolumeDataRenderAdapter(pData);
// 每次候选使用独立 ID,旧场景直到候选全部准备成功才释放。
const QString sPrefix = QString::fromLatin1("nm.volume.") + QUuid::createUuid().toString();
pCandidate->m_sMeshId = sPrefix + QLatin1String(".mesh");
pCandidate->m_sScalarBarId = sPrefix + QLatin1String(".scalar");
pCandidate->m_sBoundsId = sPrefix + QLatin1String(".bounds");
nmVTKScene* pScene = pCandidate->m_pContainer->getScene();
if(pScene == nullptr) return false;
QScopedPointer<nmVTKMeshLayer> pMesh(new nmVTKMeshLayer(pCandidate->m_sMeshId));
@ -172,16 +218,7 @@ bool nmVolumeSceneController::setData(const QSharedPointer<const nmVolumeData>&
pScalarBar->setVisible(false);
if(!pScene->addLayer(pScalarBar.data())) return false;
pCandidate->m_pScalarBar = pScalarBar.take();
QScopedPointer<nmVTKBoundsGridLayer> pBounds(new nmVTKBoundsGridLayer(pCandidate->m_sBoundsId));
pBounds->setVisible(false);
if(!pScene->addLayer(pBounds.data())) return false;
pCandidate->m_pBounds = pBounds.take();
double dBounds[6];
pData->getBounds(dBounds);
pCandidate->m_pBounds->setCamera(pCandidate->m_pContainer->getRenderer()->GetActiveCamera());
pCandidate->m_pBounds->setMode(nmVTKBoundsGridLayer::Mode_Box3D);
if(!pCandidate->m_pBounds->setBounds(dBounds[0], dBounds[1], dBounds[2], dBounds[3], dBounds[4], dBounds[5])) return false;
pCandidate->m_pMesh->setDisplayMode(nmVTKMeshLayer::DisplayMode_SurfaceWithEdges);
pCandidate->m_pMesh->setDisplayMode(pCandidate->m_eMeshDisplayMode);
pCandidate->m_pMesh->getActor()->GetProperty()->SetEdgeColor(0.0, 0.0, 0.0);
pCandidate->m_pMesh->getActor()->GetProperty()->SetColor(0.65, 0.65, 0.65);
if(pData->getInitialPropertyId().isEmpty()) {
@ -211,7 +248,7 @@ void nmVolumeSceneController::activate(bool bFitCamera)
m_pPrivate->m_bVisible = true;
m_pPrivate->updateVisibility();
m_pPrivate->m_pContainer->setPrimaryViewBounds(dBounds);
m_pPrivate->m_pContainer->setRotationCenterPickTarget(m_pPrivate->m_pMesh->getActor());
m_pPrivate->updatePickTarget();
if(bFitCamera) {
m_pPrivate->m_pContainer->fitIsometricViewWithoutRender();
m_pPrivate->m_pContainer->resetRotationCenterWithoutRender();
@ -241,14 +278,37 @@ bool nmVolumeSceneController::applyProperty(const QString& sPropertyId, QString*
if(pError != nullptr) pError->clear();
if(!hasData()) return false;
if(sPropertyId == m_pPrivate->m_sPropertyId) return true;
if(m_pPrivate->m_pSliceData != nullptr) {
nmVolumeSliceSettings oSettings = m_pPrivate->m_oSliceSettings;
oSettings.m_sPropertyId = sPropertyId;
const bool bWasEnabled = m_pPrivate->m_bSliceEnabled;
if(!applySliceSettings(oSettings, pError)) return false;
if(!bWasEnabled) setSliceEnabled(false);
return true;
}
if(!m_pPrivate->bindProperty(sPropertyId)) {
if(pError != nullptr) *pError = sceneText("Unable to bind the volume scalar property.");
return false;
}
m_pPrivate->updateVisibility();
m_pPrivate->m_oSliceSettings.m_sPropertyId = sPropertyId;
if(m_pPrivate->m_bVisible) m_pPrivate->m_pContainer->renderView();
return true;
}
void nmVolumeSceneController::setMeshDisplayMode(nmVTKMeshLayer::DisplayMode eMode)
{
if(!hasData() || !nmVTKMeshLayer::isValidDisplayMode(eMode) ||
m_pPrivate->m_eMeshDisplayMode == eMode) return;
m_pPrivate->m_eMeshDisplayMode = eMode;
m_pPrivate->updateVisibility();
if(m_pPrivate->m_bVisible) m_pPrivate->m_pContainer->renderView();
}
nmVTKMeshLayer::DisplayMode nmVolumeSceneController::getMeshDisplayMode() const
{
return m_pPrivate->m_eMeshDisplayMode;
}
void nmVolumeSceneController::setViewportSize(int nWidth, int nHeight)
{
if(hasData()) m_pPrivate->m_pScalarBar->setViewportSize(nWidth, nHeight);
@ -261,14 +321,112 @@ void nmVolumeSceneController::setScalarBarVisible(bool bVisible)
m_pPrivate->updateVisibility();
if(hasData() && m_pPrivate->m_bVisible) m_pPrivate->m_pContainer->renderView();
}
void nmVolumeSceneController::setScaleGridVisible(bool bVisible)
bool nmVolumeSceneController::isScalarBarVisible() const { return m_pPrivate->m_bScalarBarVisible; }
bool nmVolumeSceneController::hasScalarProperty() const { return hasData() && !m_pPrivate->m_sPropertyId.isEmpty(); }
QSharedPointer<const nmVolumeData> nmVolumeSceneController::getData() const
{
if(!isVolumeGuiThread()) return;
if(m_pPrivate->m_bScaleGridVisible == bVisible) return;
m_pPrivate->m_bScaleGridVisible = bVisible;
return hasData() ? m_pPrivate->m_pData : QSharedPointer<const nmVolumeData>();
}
nmVolumeSliceSettings nmVolumeSceneController::getSliceSettings() const { return m_pPrivate->m_oSliceSettings; }
bool nmVolumeSceneController::isSliceEnabled() const { return hasData() && m_pPrivate->m_bSliceEnabled; }
bool nmVolumeSceneController::isSliceEmpty() const
{
return isSliceEnabled() && m_pPrivate->m_pSliceData != nullptr && m_pPrivate->m_pSliceData->GetNumberOfCells() == 0;
}
unsigned int nmVolumeSceneController::getSliceRevision() const { return m_pPrivate->m_nSliceRevision; }
bool nmVolumeSceneController::setSliceEnabled(bool bEnabled, QString* pError)
{
if(pError != nullptr) pError->clear();
if(!hasData()) return false;
if(bEnabled && m_pPrivate->m_pSliceData == nullptr) return applySliceSettings(m_pPrivate->m_oSliceSettings, pError);
if(m_pPrivate->m_bSliceEnabled == bEnabled) return true;
m_pPrivate->m_bSliceEnabled = bEnabled;
m_pPrivate->updateVisibility();
if(hasData() && m_pPrivate->m_bVisible) m_pPrivate->m_pContainer->renderView();
m_pPrivate->updatePickTarget();
if(m_pPrivate->m_bVisible) m_pPrivate->m_pContainer->renderView();
return true;
}
void nmVolumeSceneController::setSliceDisplayOptions(bool bShowPlane, bool bShowSource)
{
if(!hasData()) return;
m_pPrivate->m_oSliceSettings.m_bShowPlane = bShowPlane;
m_pPrivate->m_oSliceSettings.m_bShowSource = bShowSource;
m_pPrivate->updateVisibility();
if(m_pPrivate->m_bVisible) m_pPrivate->m_pContainer->renderView();
}
bool nmVolumeSceneController::applySliceSettings(const nmVolumeSliceSettings& oSettings, QString* pError)
{
if(pError != nullptr) pError->clear();
if(!hasData()) return false;
nmVolumeSliceSettings oCandidateSettings = oSettings;
if(!oCandidateSettings.normalize(pError)) return false;
const nmVolumePropertyDescriptor* pProperty = m_pPrivate->m_pData->findProperty(oCandidateSettings.m_sPropertyId);
if(pProperty == nullptr && (!oCandidateSettings.m_sPropertyId.isEmpty() || !m_pPrivate->m_pData->getProperties().isEmpty())) {
if(pError != nullptr) *pError = sceneText("Unable to bind the volume scalar property.");
return false;
}
vtkSmartPointer<vtkPolyData> pCandidateData;
if(!nmVolumeSlicePipeline::build(m_pPrivate->m_pData, oCandidateSettings, pCandidateData, pError)) return false;
try {
const QString sCandidateId = QString::fromLatin1("nm.volume.slice.") + QUuid::createUuid().toString();
QScopedPointer<nmVTKMeshLayer> pCandidateMesh(new nmVTKMeshLayer(sCandidateId));
pCandidateMesh->setVisible(false);
pCandidateMesh->setDisplayMode(m_pPrivate->m_eMeshDisplayMode);
pCandidateMesh->getActor()->GetProperty()->SetColor(0.65, 0.65, 0.65);
pCandidateMesh->getActor()->GetProperty()->SetEdgeColor(0.0, 0.0, 0.0);
// 关闭光照明暗,截面颜色直接对应色标,方便定量核对。
pCandidateMesh->getActor()->GetProperty()->LightingOff();
if(!pCandidateMesh->setDataSet(pCandidateData)) return false;
bool bHasFiniteValues = false;
if(pProperty != nullptr && pCandidateData->GetNumberOfCells() > 0) {
vtkDataArray* pValues = pProperty->m_eAssociation == NM_VolumeProperty_CellData
? pCandidateData->GetCellData()->GetArray(pProperty->m_sArrayName.toUtf8().constData())
: pCandidateData->GetPointData()->GetArray(pProperty->m_sArrayName.toUtf8().constData());
for(vtkIdType nValue = 0; pValues != nullptr && nValue < pValues->GetNumberOfTuples(); ++nValue) {
if(vtkMath::IsFinite(pValues->GetComponent(nValue, 0))) { bHasFiniteValues = true; break; }
}
}
// LUT 只使用数据发布时的全局范围,不用截面的局部范围覆盖它。
vtkSmartPointer<vtkLookupTable> pTable;
if(pProperty != nullptr) {
pTable = makeVolumeLookupTable(*pProperty);
const nmVTKMeshLayer::ScalarAssociation eAssociation = pProperty->m_eAssociation == NM_VolumeProperty_CellData
? nmVTKMeshLayer::ScalarAssociation_CellData : nmVTKMeshLayer::ScalarAssociation_PointData;
if(bHasFiniteValues) {
if(!pCandidateMesh->setScalarArray(pProperty->m_sArrayName, eAssociation, pTable)) return false;
pCandidateMesh->setScalarVisibility(true);
}
}
nmVTKScene* pScene = m_pPrivate->m_pContainer->getScene();
VolumeSliceLayerGuard oGuard(pScene, sCandidateId);
if(!pScene->addLayer(pCandidateMesh.data())) return false;
nmVTKMeshLayer* pNewMesh = pCandidateMesh.take();
oGuard.hold(pNewMesh);
// 全部候选已验证。完整模型的属性绑定也按原接口原子提交,失败时撤销新图层。
if(pProperty != nullptr && !m_pPrivate->bindProperty(oCandidateSettings.m_sPropertyId, pTable)) {
if(pError != nullptr) *pError = sceneText("Unable to bind the volume scalar property.");
return false;
}
if(m_pPrivate->m_bVisible) m_pPrivate->m_pContainer->setRotationCenterPickTarget(nullptr);
if(m_pPrivate->m_pSliceMesh != nullptr) pScene->removeLayer(m_pPrivate->m_sSliceId);
m_pPrivate->m_sSliceId = sCandidateId;
m_pPrivate->m_pSliceMesh = pNewMesh;
m_pPrivate->m_pSliceData = pCandidateData;
m_pPrivate->m_oSliceSettings = oCandidateSettings;
m_pPrivate->m_bSliceEnabled = true;
oGuard.release();
++m_pPrivate->m_nSliceRevision;
m_pPrivate->updateVisibility();
m_pPrivate->updatePickTarget();
if(m_pPrivate->m_bVisible) m_pPrivate->m_pContainer->renderView();
return true;
} catch(const std::bad_alloc&) {
if(pError != nullptr) *pError = sceneText("Not enough memory to display the volume mesh.");
return false;
}
}
bool nmVolumeSceneController::isScalarBarVisible() const { return m_pPrivate->m_bScalarBarVisible; }
bool nmVolumeSceneController::isScaleGridVisible() const { return m_pPrivate->m_bScaleGridVisible; }
bool nmVolumeSceneController::hasScalarProperty() const { return hasData() && !m_pPrivate->m_sPropertyId.isEmpty(); }

@ -0,0 +1,178 @@
#include "nmVolumeSliceInteraction.h"
#include "nmWxVTKRenderContainerWidget.h"
#include <QMetaObject>
#include <vtkActor.h>
#include <vtkCallbackCommand.h>
#include <vtkCellPicker.h>
#include <vtkCommand.h>
#include <vtkImplicitPlaneRepresentation.h>
#include <vtkImplicitPlaneWidget2.h>
#include <vtkProperty.h>
#include <vtkObjectFactory.h>
#include <vtkWidgetEventTranslator.h>
namespace {
/** @brief VTK 7.1 的 DrawPlaneOff 仍拾取不可见填充面,需将拾取限定到可见轮廓和手柄。 */
class nmVolumePlaneRepresentation : public vtkImplicitPlaneRepresentation
{
public:
/** @brief 按 VTK 引用计数规则创建本地预览表示。 */
static nmVolumePlaneRepresentation* New();
vtkTypeMacro(nmVolumePlaneRepresentation, vtkImplicitPlaneRepresentation);
/** @brief 中心或轮廓沿法向移动平面,法向箭头继续使用 VTK 内建旋转。 */
virtual int ComputeInteractionState(int nX, int nY, int nModify = 0)
{
const bool bMoving = this->InteractionState == Moving;
const int nState = this->Superclass::ComputeInteractionState(nX, nY, nModify);
if(bMoving && (nState == MovingOrigin || this->Picker->GetActor() == this->EdgesActor)) {
this->SetInteractionState(Pushing);
this->SetRepresentationState(Pushing);
}
return this->InteractionState;
}
protected:
/** @brief 去除填充面和固定包围盒的拾取,避免在模型空白区域抢占相机操作。 */
nmVolumePlaneRepresentation()
{
this->Picker->DeletePickList(this->CutActor);
this->Picker->DeletePickList(this->OutlineActor);
this->Picker->AddPickList(this->EdgesActor);
}
};
vtkStandardNewMacro(nmVolumePlaneRepresentation);
/** @brief 在隐藏、导入或退出模式时安全取消未松手的拖动,不保留 Interactor 焦点。 */
class nmVolumePlaneWidget : public vtkImplicitPlaneWidget2
{
public:
/** @brief 创建本窗口独占的交互 Widget。 */
static nmVolumePlaneWidget* New();
vtkTypeMacro(nmVolumePlaneWidget, vtkImplicitPlaneWidget2);
/** @brief 禁用时先解除主动拖动及焦点,不能发出一次伪造的松手应用。 */
virtual void SetEnabled(int nEnabled)
{
if(!nEnabled && this->Enabled && this->WidgetState == Active) {
this->ReleaseFocus();
this->WidgetState = Start;
this->GetImplicitPlaneRepresentation()->SetRepresentationState(vtkImplicitPlaneRepresentation::Outside);
this->EndInteraction();
}
this->Superclass::SetEnabled(nEnabled);
}
};
vtkStandardNewMacro(nmVolumePlaneWidget);
}
nmVolumeSliceInteraction::nmVolumeSliceInteraction(nmWxVTKRenderContainerWidget* pContainer, QObject* pParent)
: QObject(pParent), m_pContainer(pContainer),
m_pRepresentation(vtkSmartPointer<nmVolumePlaneRepresentation>::New()),
m_pWidget(vtkSmartPointer<nmVolumePlaneWidget>::New()),
m_pCallback(vtkSmartPointer<vtkCallbackCommand>::New()),
m_nGeneration(0), m_nPendingGeneration(0), m_bQueued(false), m_bPendingApply(false)
{
m_pRepresentation->SetPlaceFactor(1.0);
m_pRepresentation->DrawPlaneOff();
m_pRepresentation->OutlineTranslationOff();
m_pRepresentation->ScaleEnabledOff();
m_pRepresentation->OutsideBoundsOn();
m_pRepresentation->ConstrainToWidgetBoundsOff();
m_pRepresentation->GetOutlineProperty()->SetColor(0.45, 0.55, 0.65);
m_pWidget->SetRepresentation(m_pRepresentation);
m_pWidget->KeyPressActivationOff();
// 中键、右键及键盘保留给现有相机和菜单;仅左键手柄用于移动或旋转平面。
vtkWidgetEventTranslator* pTranslator = m_pWidget->GetEventTranslator();
pTranslator->RemoveTranslation(vtkCommand::MiddleButtonPressEvent);
pTranslator->RemoveTranslation(vtkCommand::MiddleButtonReleaseEvent);
pTranslator->RemoveTranslation(vtkCommand::RightButtonPressEvent);
pTranslator->RemoveTranslation(vtkCommand::RightButtonReleaseEvent);
pTranslator->RemoveTranslation(vtkCommand::KeyPressEvent);
pTranslator->RemoveTranslation(vtkCommand::KeyReleaseEvent);
m_pCallback->SetClientData(this);
m_pCallback->SetCallback(onPlaneEvent);
m_pWidget->AddObserver(vtkCommand::InteractionEvent, m_pCallback);
m_pWidget->AddObserver(vtkCommand::EndInteractionEvent, m_pCallback);
}
nmVolumeSliceInteraction::~nmVolumeSliceInteraction()
{
unbind();
m_pWidget->RemoveObservers(vtkCommand::InteractionEvent, m_pCallback);
m_pWidget->RemoveObservers(vtkCommand::EndInteractionEvent, m_pCallback);
m_pCallback->SetClientData(nullptr);
}
void nmVolumeSliceInteraction::bind(const QString& sDataId, const double dBounds[6], const nmVolumeSliceSettings& oSettings)
{
unbind();
if(m_pContainer == nullptr || sDataId.isEmpty() || dBounds == nullptr) return;
m_sDataId = sDataId;
m_pWidget->SetInteractor(m_pContainer->getInteractor());
m_pWidget->SetCurrentRenderer(m_pContainer->getRenderer());
double dPlacement[6];
for(int nIndex = 0; nIndex < 6; ++nIndex) dPlacement[nIndex] = dBounds[nIndex];
m_pRepresentation->PlaceWidget(dPlacement);
setPreview(oSettings);
}
void nmVolumeSliceInteraction::unbind()
{
++m_nGeneration;
m_bPendingApply = false;
m_pWidget->SetEnabled(0);
m_pWidget->SetInteractor(nullptr);
m_pWidget->SetCurrentRenderer(nullptr);
m_sDataId.clear();
}
void nmVolumeSliceInteraction::setPreview(const nmVolumeSliceSettings& oSettings)
{
nmVolumeSliceSettings oNormalized = oSettings;
if(!oNormalized.normalize()) return;
// 数值输入、重置或成功应用取代旧拖动草稿,不能再交付尚未处理的旧松手事件。
++m_nGeneration;
m_bPendingApply = false;
m_oPreview = oNormalized;
m_pRepresentation->SetOrigin(m_oPreview.m_dOrigin);
m_pRepresentation->SetNormal(m_oPreview.m_dNormal);
}
void nmVolumeSliceInteraction::setEnabled(bool bEnabled)
{
bEnabled = bEnabled && !m_sDataId.isEmpty();
if(isEnabled() == bEnabled) return;
if(!bEnabled) { ++m_nGeneration; m_bPendingApply = false; }
// VTK 禁用 Widget 时会清空 CurrentRenderer;恢复时不能依赖鼠标仍停留在视口内。
if(bEnabled) m_pWidget->SetCurrentRenderer(m_pContainer->getRenderer());
m_pWidget->SetEnabled(bEnabled ? 1 : 0);
}
bool nmVolumeSliceInteraction::isEnabled() const { return m_pWidget->GetEnabled() != 0; }
void nmVolumeSliceInteraction::onPlaneEvent(vtkObject* pCaller, unsigned long nEvent, void* pClientData, void*)
{
nmVolumeSliceInteraction* pSelf = static_cast<nmVolumeSliceInteraction*>(pClientData);
if(pSelf == nullptr || !pSelf->isEnabled() || pCaller != pSelf->m_pWidget.GetPointer()) return;
pSelf->m_oPending = pSelf->m_oPreview;
pSelf->m_pRepresentation->GetOrigin(pSelf->m_oPending.m_dOrigin);
pSelf->m_pRepresentation->GetNormal(pSelf->m_oPending.m_dNormal);
pSelf->m_nPendingGeneration = pSelf->m_nGeneration;
pSelf->m_bPendingApply = pSelf->m_bPendingApply || nEvent == vtkCommand::EndInteractionEvent;
if(!pSelf->m_bQueued) {
pSelf->m_bQueued = true;
QMetaObject::invokeMethod(pSelf, "deliverPendingEvent", Qt::QueuedConnection);
}
}
void nmVolumeSliceInteraction::deliverPendingEvent()
{
m_bQueued = false;
const bool bApply = m_bPendingApply;
m_bPendingApply = false;
if(!isEnabled() || m_nPendingGeneration != m_nGeneration) return;
const unsigned int nGeneration = m_nGeneration;
const QString sDataId = m_sDataId;
m_oPreview = m_oPending;
emit previewChanged(sDataId, m_oPreview);
// previewChanged 的接收者可能关闭窗口或切换模式,必须再次检查代次。
if(bApply && isEnabled() && nGeneration == m_nGeneration) emit applyRequested(sDataId, m_oPreview);
}

@ -0,0 +1,105 @@
#include "nmVolumeSlicePipeline.h"
#include "nmVolumeDataRenderAdapter.h"
#include <QCoreApplication>
#include <new>
#include <limits>
#include <vtkAlgorithm.h>
#include <vtkCallbackCommand.h>
#include <vtkCellData.h>
#include <vtkCommand.h>
#include <vtkCutter.h>
#include <vtkDataArray.h>
#include <vtkErrorCode.h>
#include <vtkMath.h>
#include <vtkPlane.h>
#include <vtkPointData.h>
#include <vtkPolyData.h>
namespace {
/** @brief 记录 VTK 错误;回调只访问本次同步计算的栈标记,不接触界面。 */
void recordSliceError(vtkObject*, unsigned long, void* pClientData, void*)
{
*static_cast<bool*>(pClientData) = true;
}
}
bool nmVolumeSlicePipeline::build(const QSharedPointer<const nmVolumeData>& pData,
const nmVolumeSliceSettings& oSettings, vtkSmartPointer<vtkPolyData>& pOutput,
QString* pError)
{
if(pError != nullptr) pError->clear();
nmVolumeSliceSettings oNormalized = oSettings;
if(!oNormalized.normalize(pError)) return false;
if(pData.isNull()) {
if(pError != nullptr) *pError = QCoreApplication::translate("nmVolumeSlicePipeline", "No volume model is available.");
return false;
}
try {
nmVolumeDataRenderAdapter oAdapter(pData);
bool bFailed = false;
vtkSmartPointer<vtkCallbackCommand> pObserver = vtkSmartPointer<vtkCallbackCommand>::New();
pObserver->SetClientData(&bFailed);
pObserver->SetCallback(recordSliceError);
vtkSmartPointer<vtkPlane> pPlane = vtkSmartPointer<vtkPlane>::New();
pPlane->SetOrigin(oNormalized.m_dOrigin);
pPlane->SetNormal(oNormalized.m_dNormal);
vtkSmartPointer<vtkCutter> pCutter = vtkSmartPointer<vtkCutter>::New();
pCutter->SetInputConnection(oAdapter.getOutputPort());
pCutter->SetCutFunction(pPlane);
pCutter->SetValue(0, 0.0);
pCutter->GenerateCutScalarsOff();
pCutter->GenerateTrianglesOff();
pCutter->SetOutputPointsPrecision(vtkAlgorithm::DOUBLE_PRECISION);
pCutter->AddObserver(vtkCommand::ErrorEvent, pObserver);
pCutter->Update();
pCutter->RemoveObservers(vtkCommand::ErrorEvent, pObserver);
pObserver->SetClientData(nullptr);
vtkPolyData* pResult = pCutter->GetOutput();
if(!bFailed && pResult != nullptr && pCutter->GetErrorCode() == vtkErrorCode::NoError) {
for(vtkIdType nPoint = 0; nPoint < pResult->GetNumberOfPoints(); ++nPoint) {
double dPoint[3];
pResult->GetPoint(nPoint, dPoint);
if(!vtkMath::IsFinite(dPoint[0]) || !vtkMath::IsFinite(dPoint[1]) || !vtkMath::IsFinite(dPoint[2])) {
bFailed = true;
break;
}
}
// 空截面可能没有属性数组;非空截面必须完整保留发布数据的属性关联。
const QVector<nmVolumePropertyDescriptor>& vecProperties = pData->getProperties();
for(int nProperty = 0; !bFailed && pResult->GetNumberOfCells() > 0 && nProperty < vecProperties.size(); ++nProperty) {
const nmVolumePropertyDescriptor& oProperty = vecProperties.at(nProperty);
vtkDataSetAttributes* pAttributes = oProperty.m_eAssociation == NM_VolumeProperty_CellData
? static_cast<vtkDataSetAttributes*>(pResult->GetCellData())
: static_cast<vtkDataSetAttributes*>(pResult->GetPointData());
vtkDataArray* pArray = pAttributes->GetArray(oProperty.m_sArrayName.toUtf8().constData());
const vtkIdType nExpected = oProperty.m_eAssociation == NM_VolumeProperty_CellData
? pResult->GetNumberOfCells() : pResult->GetNumberOfPoints();
bFailed = pArray == nullptr || pArray->GetNumberOfComponents() != 1 || pArray->GetNumberOfTuples() != nExpected;
}
if(!bFailed) {
vtkSmartPointer<vtkPolyData> pCandidate = vtkSmartPointer<vtkPolyData>::New();
// 与临时 Cutter 完全断开;后续视图刷新不能重新执行切片。
pCandidate->DeepCopy(pResult);
// 节点插值也可能产生非有限数,只规范化独立输出,绝不写入冻结的原始数组。
for(int nProperty = 0; nProperty < vecProperties.size(); ++nProperty) {
const nmVolumePropertyDescriptor& oProperty = vecProperties.at(nProperty);
vtkDataArray* pArray = oProperty.m_eAssociation == NM_VolumeProperty_CellData
? pCandidate->GetCellData()->GetArray(oProperty.m_sArrayName.toUtf8().constData())
: pCandidate->GetPointData()->GetArray(oProperty.m_sArrayName.toUtf8().constData());
for(vtkIdType nTuple = 0; pArray != nullptr && nTuple < pArray->GetNumberOfTuples(); ++nTuple) {
if(!vtkMath::IsFinite(pArray->GetComponent(nTuple, 0))) {
pArray->SetComponent(nTuple, 0, std::numeric_limits<double>::quiet_NaN());
}
}
}
pOutput = pCandidate;
return true;
}
}
if(pError != nullptr) *pError = QCoreApplication::translate("nmVolumeSlicePipeline", "Unable to compute a valid slice. The previous view has been kept.");
} catch(const std::bad_alloc&) {
if(pError != nullptr) *pError = QCoreApplication::translate("nmVolumeSlicePipeline", "Not enough memory to compute the slice.");
}
return false;
}

@ -0,0 +1,54 @@
#include "nmVolumeSliceSettings.h"
#include "nmVolumeData.h"
#include <QCoreApplication>
#include <cmath>
#include <vtkMath.h>
nmVolumeSliceSettings::nmVolumeSliceSettings()
: m_bShowPlane(true), m_bShowSource(false)
{
for(int nAxis = 0; nAxis < 3; ++nAxis) {
m_dOrigin[nAxis] = 0.0;
m_dNormal[nAxis] = nAxis == 2 ? 1.0 : 0.0;
}
}
nmVolumeSliceSettings nmVolumeSliceSettings::fromData(const nmVolumeData& oData)
{
nmVolumeSliceSettings oSettings;
double dBounds[6];
oData.getBounds(dBounds);
for(int nAxis = 0; nAxis < 3; ++nAxis) {
// 避免两个同号大坐标相加溢出。
oSettings.m_dOrigin[nAxis] = dBounds[2 * nAxis] * 0.5 + dBounds[2 * nAxis + 1] * 0.5;
}
oSettings.m_sPropertyId = oData.getInitialPropertyId();
return oSettings;
}
bool nmVolumeSliceSettings::normalize(QString* pError)
{
if(pError != nullptr) pError->clear();
double dMaximum = 0.0;
for(int nAxis = 0; nAxis < 3; ++nAxis) {
if(!vtkMath::IsFinite(m_dOrigin[nAxis]) || !vtkMath::IsFinite(m_dNormal[nAxis])) {
if(pError != nullptr) *pError = QCoreApplication::translate("nmVolumeSliceSettings", "Origin and normal must contain finite numbers.");
return false;
}
dMaximum = qMax(dMaximum, std::fabs(m_dNormal[nAxis]));
}
if(dMaximum == 0.0) {
if(pError != nullptr) *pError = QCoreApplication::translate("nmVolumeSliceSettings", "The plane normal must not be zero.");
return false;
}
// 先缩放再求模,兼容科学计数法输入的极大、极小非零法向。
double dNormal[3];
double dLengthSquared = 0.0;
for(int nAxis = 0; nAxis < 3; ++nAxis) {
dNormal[nAxis] = m_dNormal[nAxis] / dMaximum;
dLengthSquared += dNormal[nAxis] * dNormal[nAxis];
}
const double dLength = std::sqrt(dLengthSquared);
for(int nAxis = 0; nAxis < 3; ++nAxis) m_dNormal[nAxis] = dNormal[nAxis] / dLength;
return true;
}

@ -5,6 +5,7 @@
#include "nmPebiResultPropertyDisplayText.h"
#include "nmPebiResultSceneController.h"
#include "nmVTKMeshLayer.h"
#include "nmVolumeSceneController.h"
#include "nmWxPebiResultAnimationSettingsDlg.h"
#include "nmWxPebiResultColorScaleSettingsDlg.h"
@ -57,6 +58,7 @@ nmWxPebiResultControlBar::nmWxPebiResultControlBar(
: QWidget(pParent),
m_pPlaybackController(nullptr),
m_pSceneController(pSceneController),
m_pVolumeSceneController(nullptr),
m_pDisplayModeCombo(nullptr),
m_pPropertyCombo(nullptr),
m_pColorSettingsButton(nullptr),
@ -104,11 +106,33 @@ void nmWxPebiResultControlBar::stopPlayback()
void nmWxPebiResultControlBar::synchronizeFromScene()
{
if(m_pVolumeSceneController != nullptr) {
refreshFromVolumeController();
return;
}
if(m_pPlaybackController != nullptr) {
m_pPlaybackController->synchronizeFromScene();
}
}
void nmWxPebiResultControlBar::setVolumeSceneController(
nmVolumeSceneController* pController)
{
if(m_pVolumeSceneController != pController) {
if(pController != nullptr) stopPlayback();
m_pVolumeSceneController = pController;
// 两类数据可能拥有相同属性名,切换来源时必须使列表缓存失效。
m_sDisplayedVolumeDataId.clear();
m_sDisplayedSnapshotId.clear();
m_nDisplayedBindingRevision = 0;
m_listDisplayedPropertyIds.clear();
clearTimeItems();
// 两种数据源共用 createUi 的宽度与伸缩策略,切换时不重新分配底栏空间。
m_pPropertyCombo->setToolTip(tr("Display property"));
}
synchronizeFromScene();
}
nmPebiResultPlaybackController*
nmWxPebiResultControlBar::getPlaybackController() const
{
@ -161,7 +185,7 @@ void nmWxPebiResultControlBar::onDisplayModeIndexChanged(int nIndex)
m_pPlaybackController == nullptr || m_pSceneController == nullptr ||
m_bExportInProgress || m_bExternalInteractionLocked ||
m_pPlaybackController->isPlaying() ||
!m_pSceneController->hasSnapshot() || nIndex < 0 ||
(m_pVolumeSceneController == nullptr && !m_pSceneController->hasSnapshot()) || nIndex < 0 ||
nIndex >= m_pDisplayModeCombo->count()) {
return;
}
@ -176,6 +200,13 @@ void nmWxPebiResultControlBar::onDisplayModeIndexChanged(int nIndex)
refreshFromController();
return;
}
if(m_pVolumeSceneController != nullptr) {
if(!m_pVolumeSceneController->hasData() ||
eMode == m_pVolumeSceneController->getMeshDisplayMode()) return;
m_pVolumeSceneController->setMeshDisplayMode(eMode);
emit operationSucceeded();
return;
}
if(eMode == m_pSceneController->getMeshDisplayMode()) {
return;
}
@ -185,11 +216,23 @@ void nmWxPebiResultControlBar::onDisplayModeIndexChanged(int nIndex)
void nmWxPebiResultControlBar::onPropertyIndexChanged(int nIndex)
{
if(m_pPlaybackController == nullptr || m_bExportInProgress ||
if(m_pPropertyCombo == nullptr || !m_pPropertyCombo->isEnabled() ||
nIndex >= m_pPropertyCombo->count() ||
m_pPlaybackController == nullptr || m_bExportInProgress ||
m_bExternalInteractionLocked ||
m_pPlaybackController->isPlaying() || nIndex < 0) {
return;
}
if(m_pVolumeSceneController != nullptr) {
QString sError;
const bool bApplied = m_pVolumeSceneController->applyProperty(
m_pPropertyCombo->itemData(nIndex).toString(), &sError);
// 失败时恢复到场景中仍在使用的属性,不能让下拉框与当前画面错位。
refreshFromVolumeController();
if(bApplied) emit operationSucceeded();
else emit operationFailed(sError);
return;
}
if(m_pPlaybackController->selectProperty(
m_pPropertyCombo->itemData(nIndex).toString())) {
emit operationSucceeded();
@ -198,6 +241,7 @@ void nmWxPebiResultControlBar::onPropertyIndexChanged(int nIndex)
void nmWxPebiResultControlBar::onTimeIndexChanged(int nIndex)
{
if(m_pVolumeSceneController != nullptr) return;
if(m_pPlaybackController == nullptr || m_bExportInProgress ||
m_bExternalInteractionLocked ||
m_pPlaybackController->isPlaying() || nIndex < 0) {
@ -211,6 +255,7 @@ void nmWxPebiResultControlBar::onTimeIndexChanged(int nIndex)
void nmWxPebiResultControlBar::onColorSettingsClicked()
{
if(m_pVolumeSceneController != nullptr) return;
if(m_pPlaybackController == nullptr || m_pSceneController == nullptr ||
m_bExportInProgress ||
m_bExternalInteractionLocked ||
@ -230,6 +275,7 @@ void nmWxPebiResultControlBar::onColorSettingsClicked()
void nmWxPebiResultControlBar::onFirstFrameClicked()
{
if(m_pVolumeSceneController != nullptr) return;
if(m_pPlaybackController == nullptr || m_bExportInProgress ||
m_bExternalInteractionLocked ||
m_pPlaybackController->isPlaying()) {
@ -242,6 +288,7 @@ void nmWxPebiResultControlBar::onFirstFrameClicked()
void nmWxPebiResultControlBar::onPreviousFrameClicked()
{
if(m_pVolumeSceneController != nullptr) return;
if(m_pPlaybackController == nullptr || m_bExportInProgress ||
m_bExternalInteractionLocked ||
m_pPlaybackController->isPlaying()) {
@ -254,6 +301,7 @@ void nmWxPebiResultControlBar::onPreviousFrameClicked()
void nmWxPebiResultControlBar::onNextFrameClicked()
{
if(m_pVolumeSceneController != nullptr) return;
if(m_pPlaybackController == nullptr || m_bExportInProgress ||
m_bExternalInteractionLocked ||
m_pPlaybackController->isPlaying()) {
@ -266,6 +314,7 @@ void nmWxPebiResultControlBar::onNextFrameClicked()
void nmWxPebiResultControlBar::onLastFrameClicked()
{
if(m_pVolumeSceneController != nullptr) return;
if(m_pPlaybackController == nullptr || m_bExportInProgress ||
m_bExternalInteractionLocked ||
m_pPlaybackController->isPlaying()) {
@ -278,6 +327,7 @@ void nmWxPebiResultControlBar::onLastFrameClicked()
void nmWxPebiResultControlBar::onPlayPauseClicked()
{
if(m_pVolumeSceneController != nullptr) return;
if(m_pPlaybackController == nullptr || m_bExportInProgress ||
m_bExternalInteractionLocked) {
return;
@ -289,6 +339,7 @@ void nmWxPebiResultControlBar::onPlayPauseClicked()
void nmWxPebiResultControlBar::onAnimationSettingsClicked()
{
if(m_pVolumeSceneController != nullptr) return;
if(m_pPlaybackController == nullptr || m_bExportInProgress ||
m_bExternalInteractionLocked ||
m_pPlaybackController->isPlaying()) {
@ -308,6 +359,7 @@ void nmWxPebiResultControlBar::onAnimationSettingsClicked()
void nmWxPebiResultControlBar::onExportAnimationTriggered()
{
if(m_pVolumeSceneController != nullptr) return;
if(m_pPlaybackController == nullptr || m_bExportInProgress ||
m_bExternalInteractionLocked ||
m_pPlaybackController->isPlaying() ||
@ -320,6 +372,11 @@ void nmWxPebiResultControlBar::onExportAnimationTriggered()
void nmWxPebiResultControlBar::refreshFromController()
{
// 即使旧回放控制器发来状态通知,也只能刷新当前正在查看的数据源。
if(m_pVolumeSceneController != nullptr) {
refreshFromVolumeController();
return;
}
if(m_pDisplayModeCombo != nullptr && m_pSceneController != nullptr) {
const int nDisplayModeIndex = findItemByData(
m_pDisplayModeCombo,
@ -458,6 +515,51 @@ void nmWxPebiResultControlBar::refreshFromController()
updatePlayPauseButton(bPlaying);
}
void nmWxPebiResultControlBar::refreshFromVolumeController()
{
if(m_pVolumeSceneController == nullptr) return;
const QSharedPointer<const nmVolumeData> pData = m_pVolumeSceneController->getData();
const QString sDataId = pData.isNull() ? QString() : pData->getDataId();
const bool bPropertyBlocked = m_pPropertyCombo->blockSignals(true);
if(pData.isNull() || m_sDisplayedVolumeDataId != sDataId) {
m_pPropertyCombo->clear();
if(!pData.isNull()) {
const QVector<nmVolumePropertyDescriptor>& vecProperties = pData->getProperties();
for(int nIndex = 0; nIndex < vecProperties.size(); ++nIndex) {
const nmVolumePropertyDescriptor& oProperty = vecProperties.at(nIndex);
// 仅显示原始属性名;同名单元、节点属性仍由不同 ID 区分,不合并条目。
m_pPropertyCombo->addItem(oProperty.m_sArrayName, oProperty.m_sPropertyId);
m_pPropertyCombo->setItemData(nIndex, oProperty.m_sArrayName, Qt::ToolTipRole);
}
if(vecProperties.isEmpty()) m_pPropertyCombo->addItem(tr("Solid Color"), QString());
}
m_sDisplayedVolumeDataId = sDataId;
}
m_pPropertyCombo->setCurrentIndex(findItemByData(m_pPropertyCombo,
m_pVolumeSceneController->getSliceSettings().m_sPropertyId));
m_pPropertyCombo->setToolTip(m_pPropertyCombo->currentText());
m_pPropertyCombo->blockSignals(bPropertyBlocked);
const bool bModeBlocked = m_pDisplayModeCombo->blockSignals(true);
m_pDisplayModeCombo->setCurrentIndex(findItemByData(m_pDisplayModeCombo,
static_cast<int>(m_pVolumeSceneController->getMeshDisplayMode())));
m_pDisplayModeCombo->blockSignals(bModeBlocked);
const bool bAvailable = !pData.isNull() && !m_bExportInProgress && !m_bExternalInteractionLocked;
m_pDisplayModeCombo->setEnabled(bAvailable);
m_pPropertyCombo->setEnabled(bAvailable && !pData->getProperties().isEmpty());
// 外部数据没有计算时间轴;所有旧按钮保留原位置,仅禁用不适用的操作。
clearTimeItems();
m_pColorSettingsButton->setEnabled(false);
m_pFirstButton->setEnabled(false);
m_pPreviousButton->setEnabled(false);
m_pNextButton->setEnabled(false);
m_pLastButton->setEnabled(false);
m_pPlayPauseButton->setEnabled(false);
m_pSettingsButton->setEnabled(false);
m_pExportAction->setEnabled(false);
updatePlayPauseButton(false);
}
void nmWxPebiResultControlBar::updatePlayPauseButton(bool bPlaying)
{
if(m_bDisplayedPlaying == bPlaying) {
@ -484,6 +586,7 @@ void nmWxPebiResultControlBar::createUi()
QLabel* pDisplayLabel = new QLabel(tr("Display"), this);
pDisplayLabel->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
m_pDisplayModeCombo = new QComboBox(this);
m_pDisplayModeCombo->setObjectName(QLatin1String("resultDisplayModeCombo"));
m_pDisplayModeCombo->addItem(
tr("Point"),
QVariant(static_cast<int>(
@ -516,6 +619,7 @@ void nmWxPebiResultControlBar::createUi()
pPropertyLabel->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
m_pPropertyCombo = new QComboBox(this);
m_pPropertyCombo->setObjectName(QLatin1String("resultPropertyCombo"));
m_pPropertyCombo->setMinimumWidth(PROPERTY_COMBO_MINIMUM_WIDTH);
m_pPropertyCombo->setMaximumWidth(PROPERTY_COMBO_MAXIMUM_WIDTH);
m_pPropertyCombo->setMinimumHeight(24);
@ -534,6 +638,7 @@ void nmWxPebiResultControlBar::createUi()
this, QString::fromLatin1("NmPreviousStep.png"),
tr("Previous frame"));
m_pTimeCombo = new QComboBox(this);
m_pTimeCombo->setObjectName(QLatin1String("resultTimeCombo"));
m_pTimeCombo->setMinimumWidth(TIME_COMBO_MINIMUM_WIDTH);
m_pTimeCombo->setMaximumWidth(TIME_COMBO_MAXIMUM_WIDTH);
m_pTimeCombo->setMinimumContentsLength(

@ -1,6 +1,8 @@
#include "nmWxPebiResultViewWidget.h"
#include "nmVolumeImportTask.h"
#include "nmVolumeSceneController.h"
#include "nmVolumeSliceInteraction.h"
#include "nmWxVolumeSliceSettingsDlg.h"
#include "nmVTKViewState.h"
#include <QFileDialog>
#include <QToolButton>
@ -820,6 +822,8 @@ public:
nmWxPebiVolumeState()
: m_pController(nullptr), m_pTask(nullptr), m_pProgress(nullptr),
m_pImportAction(nullptr), m_pReturnAction(nullptr), m_pButton(nullptr),
m_pSliceAction(nullptr), m_pSliceSettingsAction(nullptr),
m_pSliceDialog(nullptr), m_pSliceInteraction(nullptr),
m_bImported(false), m_bPendingSync(false), m_bRegionVisible(false),
m_bRegionDialogVisible(false) {}
nmVolumeSceneController* m_pController; ///< 窗口独占,必须在渲染容器之前析构。
@ -828,6 +832,10 @@ public:
QAction* m_pImportAction; ///< 按钮菜单和右键菜单共享的导入动作。
QAction* m_pReturnAction; ///< 仅外部模型模式启用的返回动作。
QToolButton* m_pButton; ///< 唯一新增的常驻切片工具按钮。
QAction* m_pSliceAction; ///< 按钮菜单和右键菜单共享的切片显隐动作。
QAction* m_pSliceSettingsAction; ///< 打开浮动切片参数窗口的共享动作。
nmWxVolumeSliceSettingsDlg* m_pSliceDialog; ///< 按需创建并复用的非模态参数窗口。
nmVolumeSliceInteraction* m_pSliceInteraction; ///< 窗口独占,负责辅助平面及安全事件转发。
nmVTKViewState m_oSavedView; ///< 第一次进入外部模式前捕获的计算结果视图。
nmVTKViewCommandController::ViewCommands m_eSavedCommands; ///< 原结果提供的视图命令能力。
bool m_bImported; ///< 当前画面是否为外部三维模型。
@ -870,6 +878,12 @@ nmWxPebiResultViewWidget::~nmWxPebiResultViewWidget()
m_pVolumeState->m_pTask->wait();
delete m_pVolumeState->m_pTask;
}
closeVolumeSliceSettings(true);
delete m_pVolumeState->m_pSliceDialog;
m_pVolumeState->m_pSliceDialog = nullptr;
delete m_pVolumeState->m_pSliceInteraction;
m_pVolumeState->m_pSliceInteraction = nullptr;
if(m_pControlBar != nullptr) m_pControlBar->setVolumeSceneController(nullptr);
delete m_pVolumeState->m_pController;
delete m_pVolumeState;
m_pVolumeState = nullptr;
@ -992,6 +1006,10 @@ void nmWxPebiResultViewWidget::appendContextMenuActions(QMenu* pMenu)
return;
}
pMenu->addAction(m_pVolumeState->m_pImportAction);
pMenu->addSeparator();
pMenu->addAction(m_pVolumeState->m_pSliceAction);
pMenu->addAction(m_pVolumeState->m_pSliceSettingsAction);
pMenu->addSeparator();
pMenu->addAction(m_pVolumeState->m_pReturnAction);
pMenu->addSeparator();
pMenu->addAction(m_pFilterAction);
@ -1011,7 +1029,7 @@ void nmWxPebiResultViewWidget::onControlOperationFailed(
const QString& sError)
{
// 绑定失败优先显示;输入修改不再提示,也不屏蔽后续真实操作错误。
if(!m_sBindingError.isEmpty()) {
if(!isImportedVolumeMode() && !m_sBindingError.isEmpty()) {
return;
}
m_sOperationError = sError;
@ -1020,6 +1038,17 @@ void nmWxPebiResultViewWidget::onControlOperationFailed(
void nmWxPebiResultViewWidget::onControlOperationSucceeded()
{
if(isImportedVolumeMode()) {
// 底栏的属性切换只同步已应用属性,不覆盖浮动窗口里尚未提交的平面坐标。
if(m_pVolumeState->m_pSliceDialog != nullptr) {
m_pVolumeState->m_pSliceDialog->setAppliedProperty(
m_pVolumeState->m_pController->getSliceSettings().m_sPropertyId);
}
m_sOperationError.clear();
synchronizeVolumeActions();
refreshStatusBar();
return;
}
if(m_sOperationError.isEmpty()) {
return;
}
@ -1511,10 +1540,8 @@ void nmWxPebiResultViewWidget::onScalarBarToggled(bool bVisible)
void nmWxPebiResultViewWidget::onScaleGridToggled(bool bVisible)
{
if(isImportedVolumeMode()) {
m_pVolumeState->m_pController->setScaleGridVisible(bVisible);
return;
}
// 比例网格只属于计算结果;外部模型模式不修改挂起结果的显示偏好。
if(isImportedVolumeMode()) return;
if(m_pControlBar != nullptr &&
m_pControlBar->isExternalInteractionLocked()) {
synchronizeBusinessActions();
@ -1568,6 +1595,13 @@ void nmWxPebiResultViewWidget::createUi()
nmVTKViewCommandController::ViewCommand_PrintPreview);
m_pSceneController = new nmPebiResultSceneController(m_pRenderContainer);
m_pVolumeState->m_pController = new nmVolumeSceneController(m_pRenderContainer);
m_pVolumeState->m_pSliceInteraction = new nmVolumeSliceInteraction(m_pRenderContainer, this);
connect(m_pVolumeState->m_pSliceInteraction, SIGNAL(previewChanged(QString,nmVolumeSliceSettings)),
this, SLOT(onVolumeSlicePreview(QString,nmVolumeSliceSettings)));
connect(m_pVolumeState->m_pSliceInteraction, SIGNAL(applyRequested(QString,nmVolumeSliceSettings)),
this, SLOT(onVolumeSliceDragApplied(QString,nmVolumeSliceSettings)));
connect(m_pRenderContainer, SIGNAL(rotationCenterPickingChanged(bool)),
this, SLOT(synchronizeSliceInteraction()));
connect(m_pRenderContainer, SIGNAL(viewportSizeChanged(int,int)),
this, SLOT(onViewportSizeChanged(int,int)));
const QSize oInitialViewportSize =
@ -1672,12 +1706,23 @@ void nmWxPebiResultViewWidget::createUi()
m_pVolumeState->m_pImportAction->setObjectName(QLatin1String("importVolumeAction"));
m_pVolumeState->m_pReturnAction = new QAction(tr("Return to Calculation Result"), this);
m_pVolumeState->m_pReturnAction->setObjectName(QLatin1String("returnCalculationAction"));
m_pVolumeState->m_pSliceAction = new QAction(tr("Show Slice"), this);
m_pVolumeState->m_pSliceAction->setObjectName(QLatin1String("showVolumeSliceAction"));
m_pVolumeState->m_pSliceAction->setCheckable(true);
m_pVolumeState->m_pSliceSettingsAction = new QAction(tr("Slice Settings..."), this);
m_pVolumeState->m_pSliceSettingsAction->setObjectName(QLatin1String("volumeSliceSettingsAction"));
pSliceMenu->addAction(m_pVolumeState->m_pImportAction);
pSliceMenu->addSeparator();
pSliceMenu->addAction(m_pVolumeState->m_pSliceAction);
pSliceMenu->addAction(m_pVolumeState->m_pSliceSettingsAction);
pSliceMenu->addSeparator();
pSliceMenu->addAction(m_pVolumeState->m_pReturnAction);
m_pVolumeState->m_pButton->setMenu(pSliceMenu);
m_pToolBar->addWidget(m_pVolumeState->m_pButton);
connect(m_pVolumeState->m_pImportAction, SIGNAL(triggered()), this, SLOT(onImportVolumeRequested()));
connect(m_pVolumeState->m_pReturnAction, SIGNAL(triggered()), this, SLOT(onReturnToCalculationResult()));
connect(m_pVolumeState->m_pSliceAction, SIGNAL(toggled(bool)), this, SLOT(onVolumeSliceToggled(bool)));
connect(m_pVolumeState->m_pSliceSettingsAction, SIGNAL(triggered()), this, SLOT(onShowVolumeSliceSettings()));
connect(m_pScalarBarAction, SIGNAL(toggled(bool)),
this, SLOT(onScalarBarToggled(bool)));
@ -2394,9 +2439,13 @@ void nmWxPebiResultViewWidget::synchronizeBusinessActions()
void nmWxPebiResultViewWidget::refreshStatusBar()
{
if(isImportedVolumeMode()) {
m_pStatusLabel->setStyleSheet("QLabel { color: #a32626; background: #fdecec; }");
m_pStatusLabel->setText(m_sOperationError);
m_pStatusLabel->setVisible(!m_sOperationError.isEmpty());
const bool bError = !m_sOperationError.isEmpty();
const QString sStatus = bError ? m_sOperationError :
(m_pVolumeState->m_pController->isSliceEmpty() ? tr("The slice plane does not intersect the model.") : QString());
m_pStatusLabel->setStyleSheet(bError ? "QLabel { color: #a32626; background: #fdecec; }"
: "QLabel { color: #725514; background: #fff7df; }");
m_pStatusLabel->setText(sStatus);
m_pStatusLabel->setVisible(!sStatus.isEmpty());
return;
}
if(m_pStatusLabel == nullptr || m_pSceneController == nullptr ||
@ -2458,12 +2507,21 @@ void nmWxPebiResultViewWidget::synchronizeVolumeActions()
m_pVolumeState->m_pImportAction->setText(isImportedVolumeMode()
? tr("Replace 3D Model...") : tr("Import 3D Model..."));
m_pVolumeState->m_pReturnAction->setEnabled(isImportedVolumeMode() && m_pVolumeState->m_pTask == nullptr);
const bool bSliceAvailable = isImportedVolumeMode() && m_pVolumeState->m_pTask == nullptr && m_pVolumeState->m_pController->hasData();
m_pVolumeState->m_pSliceAction->setEnabled(bSliceAvailable);
m_pVolumeState->m_pSliceSettingsAction->setEnabled(bSliceAvailable);
setActionCheckedWithoutSignal(m_pVolumeState->m_pSliceAction, isImportedVolumeMode() && m_pVolumeState->m_pController->isSliceEnabled());
if(m_pVolumeState->m_pSliceDialog != nullptr) m_pVolumeState->m_pSliceDialog->setEnabled(bSliceAvailable);
synchronizeSliceInteraction();
if(!isImportedVolumeMode()) return;
nmVolumeSceneController* pVolume = m_pVolumeState->m_pController;
// 导入任务期间冻结底栏;平时只启用当前三维场景支持的显示及属性选择。
m_pControlBar->setExternalInteractionLocked(m_pVolumeState->m_pTask != nullptr);
m_pControlBar->synchronizeFromScene();
setActionCheckedWithoutSignal(m_pScalarBarAction, pVolume->isScalarBarVisible());
setActionCheckedWithoutSignal(m_pScaleGridAction, pVolume->isScaleGridVisible());
m_pScalarBarAction->setEnabled(pVolume->hasScalarProperty());
m_pScaleGridAction->setEnabled(pVolume->hasData());
// 与筛选等结果专用功能一致,保留比例网格入口,仅在外部三维模式下禁用。
m_pScaleGridAction->setEnabled(false);
m_pWellNamesAction->setEnabled(false);
m_pFilterAction->setEnabled(false);
m_pRegionPressureAction->setEnabled(false);
@ -2535,6 +2593,8 @@ void nmWxPebiResultViewWidget::onVolumeImportFinished()
m_pStatusLabel->show();
return;
}
// 候选模型绑定成功后才释放旧切片交互;失败、取消路径均保留原截面及草稿。
closeVolumeSliceSettings(true);
if(!isImportedVolumeMode()) {
m_pVolumeState->m_oSavedView = oPreviousView;
m_pVolumeState->m_eSavedCommands = m_pRenderContainer->getViewCommandController()->getCapabilities();
@ -2554,8 +2614,8 @@ void nmWxPebiResultViewWidget::onVolumeImportFinished()
nmVTKViewCommandController::ViewCommand_Right |
nmVTKViewCommandController::ViewCommand_Isometric);
m_pVolumeState->m_bImported = true;
m_pControlBar->setExternalInteractionLocked(true);
m_pControlBar->hide();
m_pControlBar->setVolumeSceneController(m_pVolumeState->m_pController);
m_pControlBar->show();
m_pRenderContainer->restoreDefaultInteraction();
m_pVolumeState->m_pController->activate(true);
m_sOperationError.clear();
@ -2580,6 +2640,7 @@ void nmWxPebiResultViewWidget::onReturnToCalculationResult()
return;
}
}
closeVolumeSliceSettings(true);
m_pVolumeState->m_pController->hide();
m_pRenderContainer->setRotationCenterPickTarget(nullptr);
m_pVolumeState->m_bImported = false;
@ -2613,9 +2674,165 @@ void nmWxPebiResultViewWidget::onReturnToCalculationResult()
}
m_pVolumeState->m_bPendingSync = false;
m_pVolumeState->m_oSavedView = nmVTKViewState();
m_pControlBar->setVolumeSceneController(nullptr);
m_pControlBar->setExternalInteractionLocked(false);
m_pControlBar->show();
m_pControlBar->synchronizeFromScene();
synchronizeBusinessActions();
refreshStatusBar();
}
void nmWxPebiResultViewWidget::closeVolumeSliceSettings(bool bReleaseData)
{
if(m_pVolumeState == nullptr) return;
if(m_pVolumeState->m_pSliceInteraction != nullptr) m_pVolumeState->m_pSliceInteraction->unbind();
nmWxVolumeSliceSettingsDlg* pDialog = m_pVolumeState->m_pSliceDialog;
if(pDialog != nullptr) {
// 场景切换由外层统一 Render,不能由 hideEvent 在事务中途触发一次空白画面。
const bool bBlocked = pDialog->blockSignals(true);
pDialog->hide();
if(bReleaseData) pDialog->setData(QSharedPointer<const nmVolumeData>(), nmVolumeSliceSettings());
pDialog->blockSignals(bBlocked);
}
}
void nmWxPebiResultViewWidget::synchronizeSliceInteraction()
{
if(m_pVolumeState == nullptr || m_pVolumeState->m_pSliceInteraction == nullptr) return;
nmWxVolumeSliceSettingsDlg* pDialog = m_pVolumeState->m_pSliceDialog;
const bool bEnabled = isImportedVolumeMode() && m_pVolumeState->m_pTask == nullptr &&
m_pVolumeState->m_pController->isSliceEnabled() && pDialog != nullptr && pDialog->isVisible() &&
m_pVolumeState->m_pController->getSliceSettings().m_bShowPlane && !m_pRenderContainer->isRotationCenterPicking();
const bool bWasEnabled = m_pVolumeState->m_pSliceInteraction->isEnabled();
m_pVolumeState->m_pSliceInteraction->setEnabled(bEnabled);
if(bWasEnabled != m_pVolumeState->m_pSliceInteraction->isEnabled()) {
// 拾取信号也会在场景绑定过程中发出,不能在该调用栈内立即 Render。
QMetaObject::invokeMethod(this, "refreshVolumeSliceView", Qt::QueuedConnection);
}
}
void nmWxPebiResultViewWidget::refreshVolumeSliceView()
{
if(isImportedVolumeMode()) m_pRenderContainer->renderView();
}
void nmWxPebiResultViewWidget::onVolumeSliceToggled(bool bEnabled)
{
if(!isImportedVolumeMode() || m_pVolumeState->m_pTask != nullptr) { synchronizeVolumeActions(); return; }
QString sError;
if(!m_pVolumeState->m_pController->setSliceEnabled(bEnabled, &sError)) {
m_sOperationError = sError.isEmpty() ? tr("Unable to display the slice.") : sError;
} else {
m_sOperationError.clear();
if(!bEnabled) {
closeVolumeSliceSettings(false);
m_pRenderContainer->renderView();
}
}
synchronizeVolumeActions();
refreshStatusBar();
}
void nmWxPebiResultViewWidget::onShowVolumeSliceSettings()
{
if(!isImportedVolumeMode() || m_pVolumeState->m_pTask != nullptr) return;
nmVolumeSceneController* pController = m_pVolumeState->m_pController;
QString sError;
if(!pController->setSliceEnabled(true, &sError)) {
m_sOperationError = sError.isEmpty() ? tr("Unable to display the slice.") : sError;
refreshStatusBar();
return;
}
if(m_pVolumeState->m_pSliceDialog == nullptr) {
nmWxVolumeSliceSettingsDlg* pDialog = new nmWxVolumeSliceSettingsDlg(this);
m_pVolumeState->m_pSliceDialog = pDialog;
connect(pDialog, SIGNAL(applyRequested()), this, SLOT(onApplyVolumeSlice()));
connect(pDialog, SIGNAL(previewChanged()), this, SLOT(onVolumeSliceDraftChanged()));
connect(pDialog, SIGNAL(displayOptionsChanged(bool,bool)), this, SLOT(onVolumeSliceDisplayChanged(bool,bool)));
connect(pDialog, SIGNAL(dialogHidden()), this, SLOT(onVolumeSliceDialogHidden()));
}
nmWxVolumeSliceSettingsDlg* pDialog = m_pVolumeState->m_pSliceDialog;
if(!pDialog->isVisible()) {
const QSharedPointer<const nmVolumeData> pData = pController->getData();
if(pData.isNull()) return;
const nmVolumeSliceSettings oSettings = pController->getSliceSettings();
pDialog->setData(pData, oSettings);
double dBounds[6];
pData->getBounds(dBounds);
m_pVolumeState->m_pSliceInteraction->bind(pData->getDataId(), dBounds, oSettings);
}
pDialog->show();
pDialog->raise();
pDialog->activateWindow();
m_sOperationError.clear();
synchronizeVolumeActions();
refreshStatusBar();
m_pRenderContainer->renderView();
}
void nmWxPebiResultViewWidget::onApplyVolumeSlice()
{
if(!isImportedVolumeMode() || m_pVolumeState->m_pTask != nullptr ||
m_pVolumeState->m_pSliceDialog == nullptr || !m_pVolumeState->m_pSliceDialog->isVisible()) return;
nmVolumeSliceSettings oSettings;
QString sError;
if(!m_pVolumeState->m_pSliceDialog->getDraft(oSettings, &sError)) {
m_pVolumeState->m_pSliceDialog->setError(sError);
return;
}
if(!m_pVolumeState->m_pController->applySliceSettings(oSettings, &sError)) {
m_sOperationError = sError.isEmpty() ? tr("Unable to display the slice.") : sError;
m_pVolumeState->m_pSliceDialog->setError(m_sOperationError);
} else {
m_sOperationError.clear();
const nmVolumeSliceSettings oApplied = m_pVolumeState->m_pController->getSliceSettings();
m_pVolumeState->m_pSliceDialog->setAppliedSettings(oApplied);
m_pVolumeState->m_pSliceInteraction->setPreview(oApplied);
}
synchronizeVolumeActions();
refreshStatusBar();
}
void nmWxPebiResultViewWidget::onVolumeSliceDraftChanged()
{
if(!isImportedVolumeMode() || m_pVolumeState->m_pTask != nullptr || m_pVolumeState->m_pSliceDialog == nullptr) return;
nmVolumeSliceSettings oDraft;
if(m_pVolumeState->m_pSliceDialog->getDraft(oDraft)) {
m_pVolumeState->m_pSliceInteraction->setPreview(oDraft);
m_pRenderContainer->renderView();
}
}
void nmWxPebiResultViewWidget::onVolumeSliceDisplayChanged(bool bShowPlane, bool bShowSource)
{
if(!isImportedVolumeMode() || m_pVolumeState->m_pTask != nullptr) return;
m_pVolumeState->m_pController->setSliceDisplayOptions(bShowPlane, bShowSource);
synchronizeSliceInteraction();
m_pRenderContainer->renderView();
}
void nmWxPebiResultViewWidget::onVolumeSliceDialogHidden()
{
if(m_pVolumeState == nullptr || m_pVolumeState->m_pSliceInteraction == nullptr) return;
m_pVolumeState->m_pSliceInteraction->unbind();
m_pRenderContainer->renderView();
}
void nmWxPebiResultViewWidget::onVolumeSlicePreview(const QString& sDataId, const nmVolumeSliceSettings& oSettings)
{
if(!isImportedVolumeMode() || m_pVolumeState->m_pTask != nullptr ||
!m_pVolumeState->m_pController->isSliceEnabled() || m_pVolumeState->m_pSliceDialog == nullptr ||
!m_pVolumeState->m_pSliceDialog->isVisible()) return;
const QSharedPointer<const nmVolumeData> pData = m_pVolumeState->m_pController->getData();
if(pData.isNull() || pData->getDataId() != sDataId) return;
m_pVolumeState->m_pSliceDialog->setPlanePreview(oSettings);
}
void nmWxPebiResultViewWidget::onVolumeSliceDragApplied(const QString& sDataId, const nmVolumeSliceSettings& oSettings)
{
Q_UNUSED(oSettings);
if(!isImportedVolumeMode() || m_pVolumeState->m_pTask != nullptr || !m_pVolumeState->m_pController->isSliceEnabled()) return;
const QSharedPointer<const nmVolumeData> pData = m_pVolumeState->m_pController->getData();
if(pData.isNull() || pData->getDataId() != sDataId) return;
onApplyVolumeSlice();
}

@ -0,0 +1,317 @@
#include "nmWxVolumeSliceSettingsDlg.h"
#include "nmParameterField.h"
#include <QCheckBox>
#include <QGridLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QHideEvent>
#include <QLabel>
#include <QLineEdit>
#include <QPalette>
#include <QPushButton>
#include <QSizePolicy>
#include <QToolButton>
#include <QVariant>
#include <QVBoxLayout>
nmWxVolumeSliceSettingsDlg::nmWxVolumeSliceSettingsDlg(QWidget* pParent)
: iDlgBase(pParent), m_pShowPlaneCheck(nullptr),
m_pShowSourceCheck(nullptr), m_pErrorLabel(nullptr), m_pApplyButton(nullptr), m_bUpdating(false)
{
createUi();
writeSettings(m_oApplied);
resize(minimumWidth(), sizeHint().height());
}
void nmWxVolumeSliceSettingsDlg::createUi()
{
setWindowTitle(tr("Slice Settings"));
setObjectName(QLatin1String("volumeSliceSettingsDialog"));
setModal(false);
setAttribute(Qt::WA_DeleteOnClose, false);
// 复用编辑井、储层特性的基类主题,同时保留工具窗口的非模态交互及 Esc 关闭行为。
setWindowFlags((windowFlags() & ~Qt::WindowType_Mask &
~Qt::WindowContextHelpButtonHint) | Qt::Tool);
setIgnoreEsc(false);
// 六个分量各占一行,共用标签列和输入列;原点区与法向区分别放置对应快捷操作。
const int nEditorHeight = nmParameterField::editorHeight();
const int nEditorMinimumWidth = nmParameterField::editorMinimumWidth();
QGroupBox* pPlaneGroup = new QGroupBox(tr("Plane Parameters"), this);
pPlaneGroup->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
QGridLayout* pPlaneLayout = new QGridLayout(pPlaneGroup);
pPlaneLayout->setContentsMargins(8, 15, 8, 10);
pPlaneLayout->setHorizontalSpacing(6);
pPlaneLayout->setVerticalSpacing(6);
pPlaneLayout->setColumnStretch(1, 1);
int nFieldLabelWidth = 0;
const char* pAxes[3] = { "X", "Y", "Z" };
for(int nAxis = 0; nAxis < 3; ++nAxis) {
QLabel* pOriginLabel = new QLabel(tr("Origin %1").arg(QLatin1String(pAxes[nAxis])), pPlaneGroup);
QLabel* pNormalLabel = new QLabel(tr("Normal %1").arg(QLatin1String(pAxes[nAxis])), pPlaneGroup);
pOriginLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
pNormalLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
nFieldLabelWidth = qMax(nFieldLabelWidth,
qMax(pOriginLabel->sizeHint().width(), pNormalLabel->sizeHint().width()));
pPlaneLayout->addWidget(pOriginLabel, nAxis, 0);
pPlaneLayout->addWidget(pNormalLabel, nAxis + 5, 0);
m_pOriginEdits[nAxis] = new QLineEdit(pPlaneGroup);
m_pNormalEdits[nAxis] = new QLineEdit(pPlaneGroup);
m_pOriginEdits[nAxis]->setObjectName(QString::fromLatin1("sliceOrigin") + QLatin1String(pAxes[nAxis]));
m_pNormalEdits[nAxis]->setObjectName(QString::fromLatin1("sliceNormal") + QLatin1String(pAxes[nAxis]));
m_pOriginEdits[nAxis]->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
m_pNormalEdits[nAxis]->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
m_pOriginEdits[nAxis]->setMinimumWidth(nEditorMinimumWidth);
m_pNormalEdits[nAxis]->setMinimumWidth(nEditorMinimumWidth);
m_pOriginEdits[nAxis]->setFixedHeight(nEditorHeight);
m_pNormalEdits[nAxis]->setFixedHeight(nEditorHeight);
m_pOriginEdits[nAxis]->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
m_pNormalEdits[nAxis]->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
pPlaneLayout->addWidget(m_pOriginEdits[nAxis], nAxis, 1);
pPlaneLayout->addWidget(m_pNormalEdits[nAxis], nAxis + 5, 1);
connect(m_pOriginEdits[nAxis], SIGNAL(textChanged(QString)), this, SLOT(onDraftChanged()));
connect(m_pNormalEdits[nAxis], SIGNAL(textChanged(QString)), this, SLOT(onDraftChanged()));
}
// 模型中心仅调整原点,放在原点 Z 下方,保持按钮紧凑并与输入框右边界对齐。
QToolButton* pCenterButton = new QToolButton(pPlaneGroup);
pCenterButton->setText(tr("Model Center"));
pCenterButton->setAutoRaise(true);
pCenterButton->setMinimumWidth(72);
pCenterButton->setFixedHeight(nEditorHeight);
pCenterButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
pPlaneLayout->addWidget(pCenterButton, 3, 1, Qt::AlignRight);
pPlaneLayout->setRowMinimumHeight(4, 6);
connect(pCenterButton, SIGNAL(clicked()), this, SLOT(onCenterRequested()));
// 控件按轴成对创建,显式调整 Tab 顺序,使键盘输入顺序与从上到下的布局一致。
setTabOrder(m_pOriginEdits[0], m_pOriginEdits[1]);
setTabOrder(m_pOriginEdits[1], m_pOriginEdits[2]);
setTabOrder(m_pOriginEdits[2], pCenterButton);
setTabOrder(pCenterButton, m_pNormalEdits[0]);
setTabOrder(m_pNormalEdits[0], m_pNormalEdits[1]);
setTabOrder(m_pNormalEdits[1], m_pNormalEdits[2]);
// 三个方向按钮放在法向 Z 下方,等宽分配输入列空间,整体左右边界与输入框一致。
QHBoxLayout* pShortcutLayout = new QHBoxLayout;
pShortcutLayout->setContentsMargins(0, 0, 0, 0);
pShortcutLayout->setSpacing(6);
const char* pPlanes[3] = { "XY", "XZ", "YZ" };
QWidget* pPreviousShortcutWidget = m_pNormalEdits[2];
for(int nPlane = 0; nPlane < 3; ++nPlane) {
QToolButton* pButton = new QToolButton(pPlaneGroup);
pButton->setText(QString::fromLatin1(pPlanes[nPlane]));
pButton->setProperty("normalAxis", 2 - nPlane);
pButton->setAutoRaise(true);
pButton->setMinimumWidth(44);
pButton->setFixedHeight(nEditorHeight);
pButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
pShortcutLayout->addWidget(pButton, 1);
setTabOrder(pPreviousShortcutWidget, pButton);
pPreviousShortcutWidget = pButton;
connect(pButton, SIGNAL(clicked()), this, SLOT(onAxisRequested()));
}
pPlaneLayout->addLayout(pShortcutLayout, 8, 1);
// 着色属性及渲染模式统一使用旧底栏;浮动窗口只保留平面相关的显示选项。
QGroupBox* pDisplayGroup = new QGroupBox(tr("Display Options"), this);
pDisplayGroup->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
QGridLayout* pDisplayLayout = new QGridLayout(pDisplayGroup);
pDisplayLayout->setContentsMargins(8, 15, 8, 10);
pDisplayLayout->setHorizontalSpacing(6);
pDisplayLayout->setVerticalSpacing(6);
pPlaneLayout->setColumnMinimumWidth(0, nFieldLabelWidth);
pDisplayLayout->setColumnMinimumWidth(0, nFieldLabelWidth);
pDisplayLayout->setColumnStretch(1, 1);
m_pShowPlaneCheck = new QCheckBox(tr("Show Plane"), pDisplayGroup);
m_pShowSourceCheck = new QCheckBox(tr("Show Original Model"), pDisplayGroup);
m_pShowPlaneCheck->setObjectName(QLatin1String("sliceShowPlane"));
m_pShowSourceCheck->setObjectName(QLatin1String("sliceShowSource"));
// 拖动说明放在显示平面的悬停提示中,减少常驻文字对参数布局的干扰。
m_pShowPlaneCheck->setToolTip(tr("Drag the center or outline to move the plane; drag arrows to rotate."));
QHBoxLayout* pDisplayOptionsLayout = new QHBoxLayout;
pDisplayOptionsLayout->setContentsMargins(0, 0, 0, 0);
pDisplayOptionsLayout->setSpacing(12);
pDisplayOptionsLayout->addWidget(m_pShowPlaneCheck);
pDisplayOptionsLayout->addWidget(m_pShowSourceCheck);
pDisplayOptionsLayout->addStretch();
pDisplayLayout->addLayout(pDisplayOptionsLayout, 0, 1);
setTabOrder(pPreviousShortcutWidget, m_pShowPlaneCheck);
setTabOrder(m_pShowPlaneCheck, m_pShowSourceCheck);
connect(m_pShowPlaneCheck, SIGNAL(toggled(bool)), this, SLOT(onDisplayOptionsChanged()));
connect(m_pShowSourceCheck, SIGNAL(toggled(bool)), this, SLOT(onDisplayOptionsChanged()));
m_pErrorLabel = new QLabel(this);
m_pErrorLabel->setWordWrap(true);
QPalette oErrorPalette = m_pErrorLabel->palette();
oErrorPalette.setColor(QPalette::WindowText, QColor(Qt::red));
m_pErrorLabel->setPalette(oErrorPalette);
m_pErrorLabel->hide();
// 底部沿用编辑井的边距及框架图标,应用型窗口的三个操作集中靠右排列。
QHBoxLayout* pFooterLayout = new QHBoxLayout;
pFooterLayout->setContentsMargins(4, 5, 4, 5);
pFooterLayout->setSpacing(8);
pFooterLayout->addStretch();
QPushButton* pResetButton = new QPushButton(tr("Reset"), this);
m_pApplyButton = new QPushButton(tr("Apply"), this);
QPushButton* pCloseButton = new QPushButton(tr("Close"), this);
m_pApplyButton->setObjectName(QLatin1String("sliceApplyButton"));
m_pApplyButton->setDefault(true);
pResetButton->setAutoDefault(false);
pCloseButton->setAutoDefault(false);
pResetButton->setMinimumWidth(72);
m_pApplyButton->setMinimumWidth(72);
pCloseButton->setMinimumWidth(72);
pCloseButton->setIcon(zxLoadIcon("Cancel"));
pCloseButton->setIconSize(QSize(16, 16));
pFooterLayout->addWidget(pResetButton);
pFooterLayout->addWidget(m_pApplyButton);
pFooterLayout->addWidget(pCloseButton);
connect(m_pApplyButton, SIGNAL(clicked()), this, SLOT(onApplyRequested()));
connect(pResetButton, SIGNAL(clicked()), this, SLOT(onResetRequested()));
connect(pCloseButton, SIGNAL(clicked()), this, SLOT(close()));
QVBoxLayout* pMainLayout = new QVBoxLayout(this);
pMainLayout->setContentsMargins(12, 10, 12, 0);
pMainLayout->setSpacing(10);
pMainLayout->addWidget(pPlaneGroup);
pMainLayout->addWidget(pDisplayGroup);
pMainLayout->addWidget(m_pErrorLabel);
pMainLayout->addStretch();
pMainLayout->addLayout(pFooterLayout);
setMinimumWidth(400);
}
void nmWxVolumeSliceSettingsDlg::setData(const QSharedPointer<const nmVolumeData>& pData, const nmVolumeSliceSettings& oApplied)
{
m_pData = pData;
setAppliedSettings(oApplied);
}
void nmWxVolumeSliceSettingsDlg::setAppliedSettings(const nmVolumeSliceSettings& oSettings)
{
m_oApplied = oSettings;
writeSettings(oSettings);
}
void nmWxVolumeSliceSettingsDlg::setAppliedProperty(const QString& sPropertyId)
{
// 底栏属性立即生效;重置、关闭窗口及再次应用平面时也应继续使用此属性。
m_oApplied.m_sPropertyId = sPropertyId;
}
bool nmWxVolumeSliceSettingsDlg::getDraft(nmVolumeSliceSettings& oSettings, QString* pError) const
{
if(pError != nullptr) pError->clear();
if(m_pData.isNull()) return false;
nmVolumeSliceSettings oDraft = m_oApplied;
for(int nAxis = 0; nAxis < 3; ++nAxis) {
bool bOriginOk = false;
bool bNormalOk = false;
oDraft.m_dOrigin[nAxis] = m_pOriginEdits[nAxis]->text().trimmed().toDouble(&bOriginOk);
oDraft.m_dNormal[nAxis] = m_pNormalEdits[nAxis]->text().trimmed().toDouble(&bNormalOk);
if(!bOriginOk || !bNormalOk) {
if(pError != nullptr) *pError = tr("Enter valid numbers for origin and normal.");
return false;
}
}
if(!oDraft.normalize(pError)) return false;
oDraft.m_bShowPlane = m_pShowPlaneCheck->isChecked();
oDraft.m_bShowSource = m_pShowSourceCheck->isChecked();
oSettings = oDraft;
return true;
}
void nmWxVolumeSliceSettingsDlg::writeSettings(const nmVolumeSliceSettings& oSettings)
{
m_bUpdating = true;
for(int nAxis = 0; nAxis < 3; ++nAxis) {
m_pOriginEdits[nAxis]->setText(QString::number(oSettings.m_dOrigin[nAxis], 'g', 17));
m_pNormalEdits[nAxis]->setText(QString::number(oSettings.m_dNormal[nAxis], 'g', 17));
}
m_pShowPlaneCheck->setChecked(oSettings.m_bShowPlane);
m_pShowSourceCheck->setChecked(oSettings.m_bShowSource);
m_bUpdating = false;
updateValidation();
}
void nmWxVolumeSliceSettingsDlg::setPlanePreview(const nmVolumeSliceSettings& oSettings)
{
m_bUpdating = true;
for(int nAxis = 0; nAxis < 3; ++nAxis) {
m_pOriginEdits[nAxis]->setText(QString::number(oSettings.m_dOrigin[nAxis], 'g', 17));
m_pNormalEdits[nAxis]->setText(QString::number(oSettings.m_dNormal[nAxis], 'g', 17));
}
m_bUpdating = false;
updateValidation();
}
void nmWxVolumeSliceSettingsDlg::setError(const QString& sError)
{
m_pErrorLabel->setText(sError);
m_pErrorLabel->setVisible(!sError.isEmpty());
}
void nmWxVolumeSliceSettingsDlg::updateValidation()
{
nmVolumeSliceSettings oDraft;
QString sError;
m_pApplyButton->setEnabled(getDraft(oDraft, &sError));
setError(sError);
}
void nmWxVolumeSliceSettingsDlg::onDraftChanged()
{
if(m_bUpdating) return;
updateValidation();
if(m_pApplyButton->isEnabled()) emit previewChanged();
}
void nmWxVolumeSliceSettingsDlg::onAxisRequested()
{
const int nNormalAxis = sender()->property("normalAxis").toInt();
m_bUpdating = true;
for(int nAxis = 0; nAxis < 3; ++nAxis) m_pNormalEdits[nAxis]->setText(nAxis == nNormalAxis ? "1" : "0");
m_bUpdating = false;
onDraftChanged();
}
void nmWxVolumeSliceSettingsDlg::onCenterRequested()
{
if(m_pData.isNull()) return;
const nmVolumeSliceSettings oCenter = nmVolumeSliceSettings::fromData(*m_pData);
m_bUpdating = true;
for(int nAxis = 0; nAxis < 3; ++nAxis) m_pOriginEdits[nAxis]->setText(QString::number(oCenter.m_dOrigin[nAxis], 'g', 17));
m_bUpdating = false;
onDraftChanged();
}
void nmWxVolumeSliceSettingsDlg::onResetRequested()
{
writeSettings(m_oApplied);
emit previewChanged();
}
void nmWxVolumeSliceSettingsDlg::onApplyRequested()
{
updateValidation();
if(m_pApplyButton->isEnabled()) emit applyRequested();
}
void nmWxVolumeSliceSettingsDlg::onDisplayOptionsChanged()
{
if(m_bUpdating) return;
m_oApplied.m_bShowPlane = m_pShowPlaneCheck->isChecked();
m_oApplied.m_bShowSource = m_pShowSourceCheck->isChecked();
emit displayOptionsChanged(m_oApplied.m_bShowPlane, m_oApplied.m_bShowSource);
}
void nmWxVolumeSliceSettingsDlg::hideEvent(QHideEvent* pEvent)
{
// 最小化产生的系统隐藏并非用户关闭,保留草稿与绑定以便还原窗口后继续操作。
if(pEvent->spontaneous()) { iDlgBase::hideEvent(pEvent); return; }
writeSettings(m_oApplied);
iDlgBase::hideEvent(pEvent);
emit dialogHidden();
}
Loading…
Cancel
Save