diff --git a/Bin/Config/Lang/cn/nmNum_cn.qm b/Bin/Config/Lang/cn/nmNum_cn.qm
index a2b8c22b..40d091fa 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 d30a0533..a53004fd 100644
--- a/Bin/Config/Lang/cn/nmNum_cn.ts
+++ b/Bin/Config/Lang/cn/nmNum_cn.ts
@@ -3349,6 +3349,7 @@ No valid error was returned; keep the last accepted result and reduce the step.<
nmWxPebiResultControlBar
+ Solid Color纯色
Display显示
Point点
Wireframe线框
@@ -3464,6 +3465,58 @@ No valid error was returned; keep the last accepted result and reduce the step.<
%1结果动画已保存到:
%1
Unable to export the result animation.无法导出结果动画。
+
+ Import 3D Model
+ 导入三维模型
+
+
+ Import 3D Model...
+ 导入三维模型…
+
+
+ Loading 3D model...
+ 正在加载三维模型…
+
+
+ Replace 3D Model...
+ 更换三维模型…
+
+
+ Return to Calculation Result
+ 返回计算结果
+
+
+ Slice
+ 切片
+
+
+ Unable to display the 3D model.
+ 无法显示三维模型。
+
+
+ Unable to return to the latest calculation result: %1
+ 无法返回最新计算结果:%1
+
+
+ VTK Volume Mesh (*.vtk *.vtu)
+ VTK 三维体网格 (*.vtk *.vtu)
+
+
+ Show Slice
+ 显示切片
+
+
+ Slice Settings...
+ 切片设置…
+
+
+ The slice plane does not intersect the model.
+ 切片平面与模型无交集。
+
+
+ Unable to display the slice.
+ 无法显示截面。
+
nmWxPebiGridViewWidget
@@ -8968,4 +9021,182 @@ Average pressure in contour: %2 MPa
表皮系数
+
+ nmVolumeData
+
+ Not enough memory to import the volume mesh.
+ 内存不足,无法导入三维网格。
+
+
+ Only linear tetrahedron, voxel, hexahedron, wedge and pyramid volume cells are supported.
+ 当前仅支持线性四面体、体素、六面体、楔形和金字塔体单元。
+
+
+ Select a .vtk or .vtu volume mesh file.
+ 请选择 .vtk 或 .vtu 三维体网格文件。
+
+
+ Skipped invalid scalar array: %1
+ 已跳过无效标量属性:%1
+
+
+ Skipped scalar array without finite values: %1
+ 已跳过没有有限值的标量属性:%1
+
+
+ The file contains no volume mesh.
+ 文件中没有有效的三维体网格。
+
+
+ The mesh connectivity is invalid.
+ 网格连接关系无效。
+
+
+ The mesh contains an invalid point index.
+ 网格包含越界的节点索引。
+
+
+ The mesh contains non-finite coordinates.
+ 网格包含无效坐标(NaN 或无穷值)。
+
+
+ The mesh contains repeated cell vertices.
+ 网格单元包含重复顶点。
+
+
+ The mesh must have a finite extent in all three directions.
+ 三维网格必须在三个方向均具有有效范围。
+
+
+ The temporary directory path cannot be read by VTK.
+ VTK 无法读取当前临时目录路径。
+
+
+ The volume mesh file is empty or cannot be opened.
+ 三维网格文件为空或无法打开。
+
+
+ Unable to create a temporary file for this path.
+ 无法为该文件路径创建临时文件。
+
+
+ Unable to read the volume mesh file.
+ 无法读取三维网格文件。
+
+
+ VTK could not read the volume mesh: %1
+ VTK 无法读取三维网格:%1
+
+
+ Volume import canceled.
+ 已取消三维模型导入。
+
+
+
+ nmVolumeSceneController
+
+ Not enough memory to display the volume mesh.
+ 内存不足,无法显示三维网格。
+
+
+ Unable to bind the volume scalar property.
+ 无法绑定三维模型的标量属性。
+
+
+
+ nmVolumeImportTask
+
+ An unexpected error occurred while importing the volume mesh.
+ 导入三维网格时发生意外错误。
+
+
+
+ nmVolumeSliceSettings
+
+ Origin and normal must contain finite numbers.
+ 原点和法向必须为有限数值。
+
+
+ The plane normal must not be zero.
+ 平面法向不能为零向量。
+
+
+
+ nmVolumeSlicePipeline
+
+ No volume model is available.
+ 当前没有可用的三维模型。
+
+
+ Unable to compute a valid slice. The previous view has been kept.
+ 无法生成有效截面,已保留原画面。
+
+
+ Not enough memory to compute the slice.
+ 内存不足,无法计算截面。
+
+
+
+ nmWxVolumeSliceSettingsDlg
+
+ Origin %1
+ 原点 %1
+
+
+ Normal %1
+ 法向 %1
+
+
+ Plane Parameters
+ 平面参数
+
+
+ Display Options
+ 显示选项
+
+
+ Slice Settings
+ 切片设置
+
+
+ Origin
+ 原点
+
+
+ Normal
+ 法向
+
+
+ Model Center
+ 模型中心
+
+
+ Show Plane
+ 显示平面
+
+
+ Show Original Model
+ 显示原模型
+
+
+ Apply
+ 应用
+
+
+ Reset
+ 重置
+
+
+ Close
+ 关闭
+
+
+ Enter valid numbers for origin and normal.
+ 请为原点和法向输入有效数值。
+
+
+ Drag the center or outline to move the plane; drag arrows to rotate.
+ 拖动中心手柄或平面轮廓移动平面,拖动箭头旋转平面。
+
+
diff --git a/Include/nmNum/nmSubWxs/nmVolumeSceneController.h b/Include/nmNum/nmSubWxs/nmVolumeSceneController.h
index c8d1e696..2fea3457 100644
--- a/Include/nmNum/nmSubWxs/nmVolumeSceneController.h
+++ b/Include/nmNum/nmSubWxs/nmVolumeSceneController.h
@@ -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 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)
diff --git a/Include/nmNum/nmSubWxs/nmVolumeSliceInteraction.h b/Include/nmNum/nmSubWxs/nmVolumeSliceInteraction.h
new file mode 100644
index 00000000..c20c4c3c
--- /dev/null
+++ b/Include/nmNum/nmSubWxs/nmVolumeSliceInteraction.h
@@ -0,0 +1,62 @@
+#ifndef NMVOLUMESLICEINTERACTION_H
+#define NMVOLUMESLICEINTERACTION_H
+
+#include "nmSubWxs_global.h"
+#include "nmVolumeSliceSettings.h"
+#include
+#include
+
+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 m_pRepresentation; ///< 独立的预览几何,不与计算平面共享。
+ vtkSmartPointer m_pWidget; ///< 本窗口独占的 VTK 交互 Widget。
+ vtkSmartPointer 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
diff --git a/Include/nmNum/nmSubWxs/nmVolumeSlicePipeline.h b/Include/nmNum/nmSubWxs/nmVolumeSlicePipeline.h
new file mode 100644
index 00000000..96a03695
--- /dev/null
+++ b/Include/nmNum/nmSubWxs/nmVolumeSlicePipeline.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& pData,
+ const nmVolumeSliceSettings& oSettings,
+ vtkSmartPointer& pOutput,
+ QString* pError = nullptr);
+};
+
+#endif // NMVOLUMESLICEPIPELINE_H
diff --git a/Include/nmNum/nmSubWxs/nmVolumeSliceSettings.h b/Include/nmNum/nmSubWxs/nmVolumeSliceSettings.h
new file mode 100644
index 00000000..cf686cc5
--- /dev/null
+++ b/Include/nmNum/nmSubWxs/nmVolumeSliceSettings.h
@@ -0,0 +1,29 @@
+#ifndef NMVOLUMESLICESETTINGS_H
+#define NMVOLUMESLICESETTINGS_H
+
+#include "nmSubWxs_global.h"
+#include
+#include
+
+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
diff --git a/Include/nmNum/nmSubWxs/nmWxPebiResultControlBar.h b/Include/nmNum/nmSubWxs/nmWxPebiResultControlBar.h
index 617c0d66..5582fb9c 100644
--- a/Include/nmNum/nmSubWxs/nmWxPebiResultControlBar.h
+++ b/Include/nmNum/nmSubWxs/nmWxPebiResultControlBar.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。 */
diff --git a/Include/nmNum/nmSubWxs/nmWxPebiResultViewWidget.h b/Include/nmNum/nmSubWxs/nmWxPebiResultViewWidget.h
index 7c485a01..5b25a354 100644
--- a/Include/nmNum/nmSubWxs/nmWxPebiResultViewWidget.h
+++ b/Include/nmNum/nmSubWxs/nmWxPebiResultViewWidget.h
@@ -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 解除临时交互、观察者和控制条锁,不修改已准备的统计区域。 */
diff --git a/Include/nmNum/nmSubWxs/nmWxVolumeSliceSettingsDlg.h b/Include/nmNum/nmSubWxs/nmWxVolumeSliceSettingsDlg.h
new file mode 100644
index 00000000..3f8ce729
--- /dev/null
+++ b/Include/nmNum/nmSubWxs/nmWxVolumeSliceSettingsDlg.h
@@ -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& 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 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
diff --git a/Src/nmNum/nmSubWxs/nmVolumeSceneController.cpp b/Src/nmNum/nmSubWxs/nmVolumeSceneController.cpp
index a8458042..d023625d 100644
--- a/Src/nmNum/nmSubWxs/nmVolumeSceneController.cpp
+++ b/Src/nmNum/nmSubWxs/nmVolumeSceneController.cpp
@@ -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
#include
#include
-#include
+#include
+#include
#include
#include
#include
#include
-#include
+#include
+#include
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 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 pTable = vtkSmartPointer::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 pTable = vtkSmartPointer::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 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 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&
try {
QScopedPointer 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 pMesh(new nmVTKMeshLayer(pCandidate->m_sMeshId));
@@ -172,16 +218,7 @@ bool nmVolumeSceneController::setData(const QSharedPointer&
pScalarBar->setVisible(false);
if(!pScene->addLayer(pScalarBar.data())) return false;
pCandidate->m_pScalarBar = pScalarBar.take();
- QScopedPointer 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 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();
+}
+
+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 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 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 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(); }
diff --git a/Src/nmNum/nmSubWxs/nmVolumeSliceInteraction.cpp b/Src/nmNum/nmSubWxs/nmVolumeSliceInteraction.cpp
new file mode 100644
index 00000000..d2956995
--- /dev/null
+++ b/Src/nmNum/nmSubWxs/nmVolumeSliceInteraction.cpp
@@ -0,0 +1,178 @@
+#include "nmVolumeSliceInteraction.h"
+#include "nmWxVTKRenderContainerWidget.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+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::New()),
+ m_pWidget(vtkSmartPointer::New()),
+ m_pCallback(vtkSmartPointer::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(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);
+}
diff --git a/Src/nmNum/nmSubWxs/nmVolumeSlicePipeline.cpp b/Src/nmNum/nmSubWxs/nmVolumeSlicePipeline.cpp
new file mode 100644
index 00000000..d5c06fd2
--- /dev/null
+++ b/Src/nmNum/nmSubWxs/nmVolumeSlicePipeline.cpp
@@ -0,0 +1,105 @@
+#include "nmVolumeSlicePipeline.h"
+#include "nmVolumeDataRenderAdapter.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace {
+/** @brief 记录 VTK 错误;回调只访问本次同步计算的栈标记,不接触界面。 */
+void recordSliceError(vtkObject*, unsigned long, void* pClientData, void*)
+{
+ *static_cast(pClientData) = true;
+}
+}
+
+bool nmVolumeSlicePipeline::build(const QSharedPointer& pData,
+ const nmVolumeSliceSettings& oSettings, vtkSmartPointer& 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 pObserver = vtkSmartPointer::New();
+ pObserver->SetClientData(&bFailed);
+ pObserver->SetCallback(recordSliceError);
+ vtkSmartPointer pPlane = vtkSmartPointer::New();
+ pPlane->SetOrigin(oNormalized.m_dOrigin);
+ pPlane->SetNormal(oNormalized.m_dNormal);
+ vtkSmartPointer pCutter = vtkSmartPointer::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& 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(pResult->GetCellData())
+ : static_cast(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 pCandidate = vtkSmartPointer::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::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;
+}
diff --git a/Src/nmNum/nmSubWxs/nmVolumeSliceSettings.cpp b/Src/nmNum/nmSubWxs/nmVolumeSliceSettings.cpp
new file mode 100644
index 00000000..94dd4b35
--- /dev/null
+++ b/Src/nmNum/nmSubWxs/nmVolumeSliceSettings.cpp
@@ -0,0 +1,54 @@
+#include "nmVolumeSliceSettings.h"
+#include "nmVolumeData.h"
+#include
+#include
+#include
+
+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;
+}
diff --git a/Src/nmNum/nmSubWxs/nmWxPebiResultControlBar.cpp b/Src/nmNum/nmSubWxs/nmWxPebiResultControlBar.cpp
index 0c00ac64..09e0596a 100644
--- a/Src/nmNum/nmSubWxs/nmWxPebiResultControlBar.cpp
+++ b/Src/nmNum/nmSubWxs/nmWxPebiResultControlBar.cpp
@@ -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 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& 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(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(
@@ -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(
diff --git a/Src/nmNum/nmSubWxs/nmWxPebiResultViewWidget.cpp b/Src/nmNum/nmSubWxs/nmWxPebiResultViewWidget.cpp
index a56d060d..560c4b28 100644
--- a/Src/nmNum/nmSubWxs/nmWxPebiResultViewWidget.cpp
+++ b/Src/nmNum/nmSubWxs/nmWxPebiResultViewWidget.cpp
@@ -1,6 +1,8 @@
#include "nmWxPebiResultViewWidget.h"
#include "nmVolumeImportTask.h"
#include "nmVolumeSceneController.h"
+#include "nmVolumeSliceInteraction.h"
+#include "nmWxVolumeSliceSettingsDlg.h"
#include "nmVTKViewState.h"
#include
#include
@@ -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(), 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 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 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 pData = m_pVolumeState->m_pController->getData();
+ if(pData.isNull() || pData->getDataId() != sDataId) return;
+ onApplyVolumeSlice();
+}
diff --git a/Src/nmNum/nmSubWxs/nmWxVolumeSliceSettingsDlg.cpp b/Src/nmNum/nmSubWxs/nmWxVolumeSliceSettingsDlg.cpp
new file mode 100644
index 00000000..fc2e9818
--- /dev/null
+++ b/Src/nmNum/nmSubWxs/nmWxVolumeSliceSettingsDlg.cpp
@@ -0,0 +1,317 @@
+#include "nmWxVolumeSliceSettingsDlg.h"
+#include "nmParameterField.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+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& 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();
+}