diff --git a/Bin/Config/Lang/cn/nmNum_cn.qm b/Bin/Config/Lang/cn/nmNum_cn.qm
index f040dfd..2b422c0 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 8e3205f..9ec454a 100644
--- a/Bin/Config/Lang/cn/nmNum_cn.ts
+++ b/Bin/Config/Lang/cn/nmNum_cn.ts
@@ -5040,6 +5040,7 @@ Please check your input coordinates.
Right View右视图
Isometric View等轴测视图
Reset Rotation Center重置旋转中心
+ Set Rotation Center设置旋转中心
Save as Image保存图片
Copy Image复制图片
Print打印
diff --git a/Include/nmNum/nmRender/nmVTKCameraController.h b/Include/nmNum/nmRender/nmVTKCameraController.h
index 0d2a033..9b5faf2 100644
--- a/Include/nmNum/nmRender/nmVTKCameraController.h
+++ b/Include/nmNum/nmRender/nmVTKCameraController.h
@@ -40,10 +40,16 @@ public:
bool setRightView();
/** @brief 设置透视等轴视图。 */
bool setIsometricView();
- /** @brief 保持观察方向和距离,将焦点平移到指定世界坐标。 */
+ /** @brief 保存独立世界坐标旋转中心,不修改相机参数。 */
bool setRotationCenter(double dX, double dY, double dZ);
- /** @brief 将旋转中心恢复为当前可见边界中心。 */
+ /** @brief 将独立旋转中心恢复为当前可见边界中心,不修改画面。 */
bool resetRotationCenter();
+ /** @brief 返回是否已经设置有效的独立旋转中心。 */
+ bool hasRotationCenter() const;
+ /** @brief 复制当前独立旋转中心;未设置或参数无效时返回 false。 */
+ bool getRotationCenter(double dCenter[3]) const;
+ /** @brief 清除独立旋转中心状态,不修改相机。 */
+ void clearRotationCenter();
private:
Q_DISABLE_COPY(nmVTKCameraController)
@@ -62,6 +68,8 @@ private:
bool zoom(double dFactor);
vtkRenderer* m_pRenderer; ///< 非所有权指针,由渲染容器持有。
+ bool m_bHasRotationCenter;
+ double m_dRotationCenter[3]; ///< 独立于 vtkCamera::FocalPoint 的世界坐标。
};
#endif // NMVTKCAMERACONTROLLER_H
diff --git a/Include/nmNum/nmRender/nmVTKInteractionManager.h b/Include/nmNum/nmRender/nmVTKInteractionManager.h
index b02d708..c9419b9 100644
--- a/Include/nmNum/nmRender/nmVTKInteractionManager.h
+++ b/Include/nmNum/nmRender/nmVTKInteractionManager.h
@@ -5,8 +5,12 @@
#include
+class nmVTKCameraController;
+class nmVTKPivotCameraInteractorStyle;
+
class vtkInteractorStyleTrackballCamera;
class vtkInteractorStyle;
+class vtkRenderer;
class vtkRenderWindowInteractor;
/**
@@ -17,13 +21,15 @@ class vtkRenderWindowInteractor;
class NM_RENDER_EXPORT nmVTKInteractionManager
{
public:
- /** @brief 创建当前实例独占的默认 TrackballCamera 样式。 */
- nmVTKInteractionManager();
+ /** @brief 创建当前实例独占的默认枢轴 TrackballCamera 样式。 */
+ explicit nmVTKInteractionManager(
+ nmVTKCameraController* pCameraController = nullptr);
/** @brief 先解除 InteractorStyle,再释放默认样式。 */
~nmVTKInteractionManager();
- /** @brief 绑定非所有权 Interactor,并立即安装默认样式。 */
- bool bindInteractor(vtkRenderWindowInteractor* pInteractor);
+ /** @brief 绑定非所有权 Interactor 和主 Renderer,并立即安装默认样式。 */
+ bool bindInteractor(vtkRenderWindowInteractor* pInteractor,
+ vtkRenderer* pRenderer = nullptr);
/**
* @brief 安装并持有一个活动交互样式;传入对象通过 VTK 引用计数共享。
* @return Interactor 和样式均有效时返回 true。
@@ -42,12 +48,14 @@ public:
vtkInteractorStyleTrackballCamera* getDefaultStyle() const;
/** @brief 返回当前活动样式的非所有权指针;未安装时返回 nullptr。 */
vtkInteractorStyle* getActiveStyle() const;
+ /** @brief 缓存状态和 Interactor 实际状态都指向默认样式时返回 true。 */
+ bool isDefaultInteractionActive() const;
private:
Q_DISABLE_COPY(nmVTKInteractionManager)
vtkRenderWindowInteractor* m_pInteractor; ///< 非所有权指针。
- vtkSmartPointer m_pDefaultStyle; ///< 当前实例独占。
+ vtkSmartPointer m_pDefaultStyle; ///< 当前实例独占。
vtkSmartPointer m_pActiveStyle; ///< 保持当前活动样式的 VTK 引用。
};
diff --git a/Include/nmNum/nmRender/nmVTKPivotCameraInteractorStyle.h b/Include/nmNum/nmRender/nmVTKPivotCameraInteractorStyle.h
new file mode 100644
index 0000000..8eaaf69
--- /dev/null
+++ b/Include/nmNum/nmRender/nmVTKPivotCameraInteractorStyle.h
@@ -0,0 +1,54 @@
+#ifndef NMVTKPIVOTCAMERAINTERACTORSTYLE_H
+#define NMVTKPIVOTCAMERAINTERACTORSTYLE_H
+
+#include "nmRender_global.h"
+
+#include
+
+class nmVTKCameraController;
+
+/**
+ * @brief 使用独立世界坐标枢轴的 TrackballCamera 交互样式。
+ *
+ * Rotate 和 Spin 将相机位置、焦点及 ViewUp 作为刚体绕独立枢轴变换,
+ * 不要求枢轴等于 vtkCamera::FocalPoint。Pan、Dolly 和滚轮行为继续复用
+ * VTK 7.1 的默认实现。
+ */
+class NM_RENDER_EXPORT nmVTKPivotCameraInteractorStyle
+ : public vtkInteractorStyleTrackballCamera
+{
+public:
+ static nmVTKPivotCameraInteractorStyle* New();
+ vtkTypeMacro(nmVTKPivotCameraInteractorStyle,
+ vtkInteractorStyleTrackballCamera);
+
+ /** @brief 绑定旋转中心状态提供者;不增加其所有权。 */
+ void setCameraController(nmVTKCameraController* pController);
+ /** @brief 返回当前旋转中心状态提供者的非所有权指针。 */
+ nmVTKCameraController* getCameraController() const;
+
+ /** @brief 按 VTK Trackball 灵敏度绕独立枢轴执行水平和垂直旋转。 */
+ virtual void Rotate();
+ /** @brief 绕穿过独立枢轴的观察方向轴执行屏幕内旋转。 */
+ virtual void Spin();
+
+protected:
+ nmVTKPivotCameraInteractorStyle();
+ virtual ~nmVTKPivotCameraInteractorStyle();
+
+private:
+ nmVTKPivotCameraInteractorStyle(
+ const nmVTKPivotCameraInteractorStyle&);
+ void operator=(const nmVTKPivotCameraInteractorStyle&);
+
+ /** @brief 查询与当前 Renderer 匹配的有限旋转中心。 */
+ bool getCurrentRotationCenter(double dCenter[3]) const;
+ /** @brief 写回相机姿态、维护裁剪和灯光,并提交一次交互渲染。 */
+ void finishCameraMotion(const double dPosition[3],
+ const double dFocalPoint[3],
+ const double dViewUp[3]);
+
+ nmVTKCameraController* m_pCameraController; ///< 非所有权指针。
+};
+
+#endif // NMVTKPIVOTCAMERAINTERACTORSTYLE_H
diff --git a/Include/nmNum/nmRender/nmVTKRotationCenterMarkerLayer.h b/Include/nmNum/nmRender/nmVTKRotationCenterMarkerLayer.h
new file mode 100644
index 0000000..671c528
--- /dev/null
+++ b/Include/nmNum/nmRender/nmVTKRotationCenterMarkerLayer.h
@@ -0,0 +1,56 @@
+#ifndef NMVTKROTATIONCENTERMARKERLAYER_H
+#define NMVTKROTATIONCENTERMARKERLAYER_H
+
+#include "nmVTKRenderLayer.h"
+
+class nmVTKRotationCenterMarkerLayerPrivate;
+class vtkRenderer;
+
+/**
+ * @brief 以固定屏幕尺度显示当前相机旋转中心的三轴标记图层。
+ *
+ * 标记只包含 X/Y/Z 三条世界坐标轴线。逻辑中心由调用方显式设置,
+ * 不读取或修改 vtkCamera::FocalPoint。显示几何会沿观察方向轻微前移
+ * 以避免共面闪烁。图层不主动渲染,所有
+ * 接口只能在 Qt GUI 线程调用。
+ */
+class NM_RENDER_EXPORT nmVTKRotationCenterMarkerLayer
+ : public nmVTKRenderLayer
+{
+public:
+ /** @brief 创建初始隐藏的旋转中心标记图层。 */
+ explicit nmVTKRotationCenterMarkerLayer(const QString& sLayerId);
+ /** @brief 先解除相机观察关系,再释放标记渲染管线。 */
+ virtual ~nmVTKRotationCenterMarkerLayer();
+
+ /**
+ * @brief 绑定 Scene 已挂载的主 Renderer,不增加其引用计数。
+ * @return Renderer 与当前图层挂载对象一致且相机观察关系建立成功时返回 true。
+ */
+ bool bindRenderer(vtkRenderer* pRenderer);
+ /** @brief 幂等移除 Renderer/Camera 观察关系及其非所有权引用。 */
+ void unbindRenderer();
+
+ /**
+ * @brief 按已保存世界中心、当前相机方向和视口像素尺度更新标记几何。
+ * @note 本函数只更新 PolyData,不主动触发 Render。
+ */
+ bool synchronize();
+
+ /** @brief 保存独立世界坐标中心并更新已有几何,不主动渲染。 */
+ bool setWorldCenter(double dX, double dY, double dZ);
+ /** @brief 返回是否已经保存有效的独立世界坐标中心。 */
+ bool hasWorldCenter() const;
+
+ /** @brief 修改标记显隐;无有效中心时不会显示,也不主动渲染。 */
+ void setMarkerVisible(bool bVisible);
+ /** @brief 返回调用方最后设置的标记显隐状态。 */
+ bool isMarkerVisible() const;
+
+private:
+ Q_DISABLE_COPY(nmVTKRotationCenterMarkerLayer)
+
+ nmVTKRotationCenterMarkerLayerPrivate* m_pPrivate; ///< 独占实现对象。
+};
+
+#endif // NMVTKROTATIONCENTERMARKERLAYER_H
diff --git a/Include/nmNum/nmRender/nmVTKRotationCenterPicker.h b/Include/nmNum/nmRender/nmVTKRotationCenterPicker.h
new file mode 100644
index 0000000..a1c5a2b
--- /dev/null
+++ b/Include/nmNum/nmRender/nmVTKRotationCenterPicker.h
@@ -0,0 +1,96 @@
+#ifndef NMVTKROTATIONCENTERPICKER_H
+#define NMVTKROTATIONCENTERPICKER_H
+
+#include "nmRender_global.h"
+
+#include
+#include
+
+class QEvent;
+class QMouseEvent;
+class QVTKWidget;
+
+class vtkProp;
+class vtkRenderer;
+
+/**
+ * @brief 在指定主几何上执行一次性旋转中心拾取。
+ *
+ * 本类不拥有 Qt 控件、Renderer 或拾取目标,不修改相机且不主动渲染。
+ * Qt 事件由所属渲染容器同步转发,拾取完成后由容器统一处理结果。
+ */
+class NM_RENDER_EXPORT nmVTKRotationCenterPicker
+{
+public:
+ enum PickOutcome
+ {
+ PickOutcome_None = 0,
+ PickOutcome_Hit,
+ PickOutcome_Miss,
+ PickOutcome_Cancelled
+ };
+
+ struct PickEventResult
+ {
+ PickEventResult(bool bEventConsumed = false,
+ PickOutcome ePickOutcome = PickOutcome_None)
+ : bConsumed(bEventConsumed),
+ eOutcome(ePickOutcome)
+ {
+ }
+
+ bool bConsumed;
+ PickOutcome eOutcome;
+ };
+
+ nmVTKRotationCenterPicker();
+ ~nmVTKRotationCenterPicker();
+
+ /** @brief 设置唯一可拾取的主几何;使用弱引用,不延长 Prop 生命周期。 */
+ void setPickTarget(vtkProp* pTarget);
+ vtkProp* getPickTarget() const;
+ bool hasPickTarget() const;
+
+ /**
+ * @brief 进入一次性拾取模式并切换十字光标。
+ * @return 控件、Renderer、目标和状态均有效时返回 true。
+ */
+ bool start(QVTKWidget* pWidget, vtkRenderer* pRenderer);
+ /** @brief 主动取消当前操作,清除待消费 Release 并恢复原光标。 */
+ PickEventResult cancel();
+ /** @brief 无语义清理临时状态,供失焦、清场和析构调用。 */
+ void reset();
+
+ bool isActive() const;
+ bool hasPendingRelease() const;
+
+ /**
+ * @brief 同步处理一个 Qt 事件。
+ *
+ * 左、右键 Press 会结束本次模式,但对应 Release 仍由本对象消费。
+ */
+ PickEventResult handleEvent(QEvent* pEvent);
+
+ /** @brief 复制最近一次 Hit 的世界坐标;没有有效命中时返回 false。 */
+ bool getPickedPosition(double dPosition[3]) const;
+
+private:
+ Q_DISABLE_COPY(nmVTKRotationCenterPicker)
+
+ bool pickAt(const QMouseEvent* pMouseEvent);
+ void finishActiveMode();
+ void restoreCursor();
+
+ vtkWeakPointer m_pPickTarget;
+ QVTKWidget* m_pWidget; ///< 非所有权指针,仅在活动模式期间有效。
+ vtkRenderer* m_pRenderer; ///< 非所有权指针,仅在活动模式期间有效。
+ QCursor m_oPreviousCursor;
+ int m_nPendingReleaseButton;
+ bool m_bHadExplicitCursor;
+ bool m_bCursorOverridden;
+ bool m_bActive;
+ bool m_bHasPickedPosition;
+ double m_dPickedPosition[3];
+};
+
+#endif // NMVTKROTATIONCENTERPICKER_H
diff --git a/Include/nmNum/nmRender/nmVTKViewCommandController.h b/Include/nmNum/nmRender/nmVTKViewCommandController.h
index d8652fe..3a48f5f 100644
--- a/Include/nmNum/nmRender/nmVTKViewCommandController.h
+++ b/Include/nmNum/nmRender/nmVTKViewCommandController.h
@@ -36,7 +36,8 @@ public:
ViewCommand_SaveImage = 0x0100,
ViewCommand_CopyImage = 0x0200,
ViewCommand_Print = 0x0400,
- ViewCommand_PrintPreview = 0x0800
+ ViewCommand_PrintPreview = 0x0800,
+ ViewCommand_SetRotationCenter = 0x1000
};
Q_DECLARE_FLAGS(ViewCommands, ViewCommand)
@@ -49,8 +50,6 @@ public:
QObject* pParent = nullptr);
virtual ~nmVTKViewCommandController();
- /** @brief 设置当前视图对外提供的命令集合。 */
- void setCapabilities(ViewCommands eCommands);
/** @brief 返回当前命令集合。 */
ViewCommands getCapabilities() const;
/** @brief 返回长期持有的 QAction;调用方不得删除或修改其父对象。 */
@@ -61,31 +60,43 @@ public:
/** @brief 按统一顺序构造右键菜单,并同步请求业务层追加扩展动作。 */
void populateContextMenu(QMenu* pMenu) const;
- /** @brief 就绪状态启用命令,空、错误和加载状态统一禁用命令。 */
- void setViewAvailable(bool bAvailable);
+ /** @brief 返回所属容器是否处于可显示画面的就绪状态。 */
bool isViewAvailable() const;
signals:
/** @brief 在视图命令和打印命令之间同步追加业务 QAction。 */
void contextMenuAboutToShow(QMenu* pMenu) const;
+private slots:
+ /** @brief 仅供容器同步一次性拾取状态,不向容器反向发起请求。 */
+ void setRotationCenterPicking(bool bPicking);
+
private:
+ friend class nmWxVTKRenderContainerWidget;
+
Q_DISABLE_COPY(nmVTKViewCommandController)
- enum { ViewCommandCount = 12 };
+ enum { ViewCommandCount = 13 };
+ /** @brief 仅供容器设置当前视图对外提供的命令集合。 */
+ void setCapabilities(ViewCommands eCommands);
+ /** @brief 仅供容器同步就绪、空、错误和加载状态。 */
+ void setViewAvailable(bool bAvailable);
QAction* createAction(ViewCommand eCommand,
const QString& sText,
const QString& sIconFile,
const char* pSlot);
int actionIndex(ViewCommand eCommand) const;
bool supports(ViewCommand eCommand) const;
+ /** @brief 仅供容器同步拾取目标、标记和交互模式的综合可用状态。 */
+ void setRotationCenterPickingAvailable(bool bAvailable);
void updateActionStates();
nmWxVTKRenderContainerWidget* m_pContainer; ///< 非所有权命令接收者。
QAction* m_pActions[ViewCommandCount]; ///< 本对象通过 Qt 父子关系拥有。
ViewCommands m_eCapabilities;
bool m_bViewAvailable;
+ bool m_bRotationCenterPickingAvailable;
};
Q_DECLARE_OPERATORS_FOR_FLAGS(nmVTKViewCommandController::ViewCommands)
diff --git a/Include/nmNum/nmRender/nmWxVTKRenderContainerWidget.h b/Include/nmNum/nmRender/nmWxVTKRenderContainerWidget.h
index bde540a..3836b4c 100644
--- a/Include/nmNum/nmRender/nmWxVTKRenderContainerWidget.h
+++ b/Include/nmNum/nmRender/nmWxVTKRenderContainerWidget.h
@@ -18,8 +18,11 @@ class nmVTKCameraController;
class nmVTKImageExporter;
class nmVTKInteractionManager;
class nmVTKOrientationMarkerController;
+class nmVTKRotationCenterMarkerLayer;
+class nmVTKRotationCenterPicker;
class nmVTKScene;
+class vtkProp;
class vtkRenderer;
class vtkRenderWindow;
class vtkRenderWindowInteractor;
@@ -28,8 +31,9 @@ class vtkInteractorStyle;
/**
* @brief 可组合的 Qt + VTK 通用渲染容器。
*
- * 容器独占 Renderer、Scene、相机、交互和方向轴服务,但不创建任何业务 Actor。
- * 调用方通过 Scene 组织图层,并在一批修改完成后显式调用 renderView。
+ * 容器独占 Renderer、业务 Scene、系统 Scene、相机、交互、方向轴和旋转
+ * 中心拾取服务,但不创建任何业务 Actor。调用方通过公开的业务 Scene 组织
+ * 图层;系统 Scene 仅管理容器内部装饰,业务清场不会删除它们。
*/
class NM_RENDER_EXPORT nmWxVTKRenderContainerWidget : public QWidget
{
@@ -38,7 +42,7 @@ class NM_RENDER_EXPORT nmWxVTKRenderContainerWidget : public QWidget
public:
/** @brief 创建独立的 VTK 显示外壳,并初始化场景、相机、交互和方向轴服务。 */
explicit nmWxVTKRenderContainerWidget(QWidget* pParent = nullptr);
- /** @brief 按命令、方向轴、交互、图层、相机和 Renderer 的顺序解除资源。 */
+ /** @brief 按命令、拾取、方向轴、交互、图层、相机和 Renderer 顺序解除资源。 */
virtual ~nmWxVTKRenderContainerWidget();
/** @brief 返回 Qt 承载控件;调用方不得删除或重新设置父对象。 */
@@ -52,7 +56,7 @@ public:
* @warning 不得直接替换其 InteractorStyle,应使用容器的交互模式接口。
*/
vtkRenderWindowInteractor* getInteractor() const;
- /** @brief 返回当前容器独占的 Scene 非所有权指针。 */
+ /** @brief 返回业务 Scene;clear() 不影响容器内部系统图层。 */
nmVTKScene* getScene() const;
/** @brief 返回当前容器独占的通用命令控制器非所有权指针。 */
nmVTKViewCommandController* getViewCommandController() const;
@@ -64,6 +68,13 @@ public:
void setViewCommandCapabilities(
nmVTKViewCommandController::ViewCommands eCommands);
+ /** @brief 注册唯一可拾取几何;传入 nullptr 会取消模式并隐藏中心标记。 */
+ void setRotationCenterPickTarget(vtkProp* pTarget);
+ /** @brief 返回当前是否正在等待用户选择旋转中心。 */
+ bool isRotationCenterPicking() const;
+ /** @brief 隐藏旋转中心标记,不修改独立中心或相机且不主动渲染。 */
+ void hideRotationCenterMarker();
+
/** @brief 安装临时交互样式,并由容器保持其 VTK 引用。 */
bool installInteractionStyle(vtkInteractorStyle* pStyle);
/** @brief 将 Interactor 恢复为当前容器独占的默认相机交互样式。 */
@@ -79,6 +90,8 @@ public:
* 供业务层批量替换几何时使用,调用方应在整批更新结束后统一 renderView。
*/
bool fitTopViewWithoutRender();
+ /** @brief 将独立旋转中心设为可见边界中心,但不修改画面或立即渲染。 */
+ bool resetRotationCenterWithoutRender();
/** @brief 切换到 VTK 页面并提交一次渲染。 */
void showReadyState();
/** @brief 显示无数据状态,消息由业务调用方提供。 */
@@ -94,6 +107,8 @@ signals:
* @warning 接收方不得保存 pMenu;信号返回后菜单即由容器继续管理。
*/
void contextMenuAboutToShow(QMenu* pMenu);
+ /** @brief 将容器确认后的拾取状态同步给公共 QAction。 */
+ void rotationCenterPickingChanged(bool bPicking);
public slots:
/** @brief 按固定倍率 1.2 放大,成功后统一渲染一次。 */
@@ -112,16 +127,20 @@ public slots:
void setRightView();
/** @brief 设置透视等轴视图。 */
void setIsometricView();
- /** @brief 平移相机焦点和位置,将指定世界坐标设为旋转中心。 */
+ /** @brief 设置独立世界坐标旋转中心,不移动当前画面。 */
void setRotationCenter(double dX, double dY, double dZ);
- /** @brief 将旋转中心恢复为当前可见边界的中心。 */
+ /** @brief 将旋转中心恢复为当前可见边界中心,不移动当前画面。 */
void resetRotationCenter();
+ /** @brief 请求开始或取消一次性旋转中心拾取模式。 */
+ void setRotationCenterPickingEnabled(bool bEnabled);
protected:
- /** @brief 只消费 QVTKWidget 的右键按下事件,其余事件保持原分发链。 */
+ /** @brief 优先处理拾取事件,再处理 QVTKWidget 的右键菜单。 */
virtual bool eventFilter(QObject* pObject, QEvent* pEvent);
private slots:
+ /** @brief 焦点切换完成后取消仍处于活动或待消费释放状态的拾取。 */
+ void cancelRotationCenterPickingAfterFocusOut();
/** @brief 弹出文件对话框并保存当前 RenderWindow 图像。 */
void saveImage();
/** @brief 将当前 RenderWindow 图像复制到系统剪贴板。 */
@@ -144,6 +163,23 @@ private:
/** @brief 统一设置非就绪状态的文字、颜色和当前页面。 */
void showMessageState(ViewState eState, const QString& sMessage);
+ /** @brief 修改独立中心和可选标记但不渲染,由外层统一提交画面。 */
+ bool applyRotationCenter(double dX,
+ double dY,
+ double dZ,
+ bool bShowMarker);
+ /** @brief 取消模式、恢复光标并同步 QAction,不修改既有中心标记。 */
+ void cancelRotationCenterPicking();
+ /** @brief 处理一次拾取事件;返回 true 表示事件必须停止向 VTK 传播。 */
+ bool handleRotationCenterPickEvent(QEvent* pEvent);
+ /** @brief 仅同步 Marker 世界几何,不主动渲染。 */
+ void synchronizeRotationCenterMarker();
+ /** @brief Ready、能力、主目标及独立中心均有效时显示标记。 */
+ void updateRotationCenterMarkerVisibility();
+ /** @brief 按目标、Marker 和当前交互样式同步命令的额外可用条件。 */
+ void updateRotationCenterCommandAvailability();
+ /** @brief 通过系统 Scene 稳定 ID 校验 Marker Layer 非所有权指针。 */
+ nmVTKRotationCenterMarkerLayer* getRotationCenterMarkerLayer() const;
/** @brief 判断调用线程是否为 Qt GUI 线程。 */
bool isGuiThread() const;
@@ -156,12 +192,15 @@ private:
// Renderer 与通用服务均为当前容器实例独占,不与其他视图共享状态。
vtkSmartPointer m_pRenderer;
- nmVTKScene* m_pScene;
+ nmVTKScene* m_pScene; ///< 公开给业务控制器的图层集合。
+ nmVTKScene* m_pSystemScene; ///< 容器私有装饰图层集合,不对外暴露。
nmVTKCameraController* m_pCameraController;
nmVTKInteractionManager* m_pInteractionManager;
nmVTKImageExporter* m_pImageExporter;
nmVTKViewCommandController* m_pViewCommandController;
nmVTKOrientationMarkerController* m_pOrientationMarkerController;
+ nmVTKRotationCenterPicker* m_pRotationCenterPicker; ///< 本容器独占。
+ nmVTKRotationCenterMarkerLayer* m_pRotationCenterMarkerLayer; ///< 系统 Scene 所有。
};
#endif // NMWXVTKRENDERCONTAINERWIDGET_H
diff --git a/Src/nmNum/nmRender/nmVTKCameraController.cpp b/Src/nmNum/nmRender/nmVTKCameraController.cpp
index eba8ff4..b4f3efb 100644
--- a/Src/nmNum/nmRender/nmVTKCameraController.cpp
+++ b/Src/nmNum/nmRender/nmVTKCameraController.cpp
@@ -20,9 +20,13 @@ bool isCameraGuiThread()
}
nmVTKCameraController::nmVTKCameraController(vtkRenderer* pRenderer)
- : m_pRenderer(pRenderer)
+ : m_pRenderer(pRenderer),
+ m_bHasRotationCenter(false)
{
Q_ASSERT(isCameraGuiThread());
+ m_dRotationCenter[0] = 0.0;
+ m_dRotationCenter[1] = 0.0;
+ m_dRotationCenter[2] = 0.0;
}
nmVTKCameraController::~nmVTKCameraController()
@@ -36,6 +40,9 @@ void nmVTKCameraController::setRenderer(vtkRenderer* pRenderer)
{
Q_ASSERT(isCameraGuiThread());
if(isCameraGuiThread()) {
+ if(m_pRenderer != pRenderer) {
+ clearRotationCenter();
+ }
m_pRenderer = pRenderer;
}
}
@@ -129,22 +136,11 @@ bool nmVTKCameraController::setRotationCenter(
return false;
}
- vtkCamera* pCamera = m_pRenderer->GetActiveCamera();
- if(pCamera == nullptr) {
- return false;
- }
-
- double dOldFocalPoint[3];
- double dOldPosition[3];
- pCamera->GetFocalPoint(dOldFocalPoint);
- pCamera->GetPosition(dOldPosition);
- pCamera->SetFocalPoint(dX, dY, dZ);
- // 同步平移相机位置,保持原观察方向和相机到焦点的距离。
- pCamera->SetPosition(
- dOldPosition[0] + dX - dOldFocalPoint[0],
- dOldPosition[1] + dY - dOldFocalPoint[1],
- dOldPosition[2] + dZ - dOldFocalPoint[2]);
- m_pRenderer->ResetCameraClippingRange();
+ // 旋转中心是独立交互状态;选择中心时不得改写当前相机画面。
+ m_dRotationCenter[0] = dX;
+ m_dRotationCenter[1] = dY;
+ m_dRotationCenter[2] = dZ;
+ m_bHasRotationCenter = true;
return true;
}
@@ -155,12 +151,33 @@ bool nmVTKCameraController::resetRotationCenter()
if(!isCameraGuiThread() || !getVisibleBounds(dBounds)) {
return false;
}
- // 复用平移逻辑,保持当前观察方向、投影方式和观察距离不变。
+ // 只更新独立枢轴;当前相机 Position/FocalPoint/ViewUp 保持不变。
return setRotationCenter((dBounds[0] + dBounds[1]) * 0.5,
(dBounds[2] + dBounds[3]) * 0.5,
(dBounds[4] + dBounds[5]) * 0.5);
}
+bool nmVTKCameraController::hasRotationCenter() const
+{
+ return m_bHasRotationCenter;
+}
+
+bool nmVTKCameraController::getRotationCenter(double dCenter[3]) const
+{
+ if(dCenter == nullptr || !m_bHasRotationCenter) {
+ return false;
+ }
+ dCenter[0] = m_dRotationCenter[0];
+ dCenter[1] = m_dRotationCenter[1];
+ dCenter[2] = m_dRotationCenter[2];
+ return true;
+}
+
+void nmVTKCameraController::clearRotationCenter()
+{
+ m_bHasRotationCenter = false;
+}
+
bool nmVTKCameraController::getVisibleBounds(double dBounds[6]) const
{
if(m_pRenderer == nullptr || dBounds == nullptr) {
diff --git a/Src/nmNum/nmRender/nmVTKInteractionManager.cpp b/Src/nmNum/nmRender/nmVTKInteractionManager.cpp
index 0c4e3bb..4b9003d 100644
--- a/Src/nmNum/nmRender/nmVTKInteractionManager.cpp
+++ b/Src/nmNum/nmRender/nmVTKInteractionManager.cpp
@@ -1,10 +1,12 @@
#include "nmVTKInteractionManager.h"
+#include "nmVTKPivotCameraInteractorStyle.h"
+
#include
#include
#include
-#include
+#include
#include
namespace {
@@ -19,13 +21,15 @@ bool isInteractionGuiThread()
}
-nmVTKInteractionManager::nmVTKInteractionManager()
+nmVTKInteractionManager::nmVTKInteractionManager(
+ nmVTKCameraController* pCameraController)
: m_pInteractor(nullptr),
m_pDefaultStyle(
- vtkSmartPointer::New()),
+ vtkSmartPointer::New()),
m_pActiveStyle(nullptr)
{
Q_ASSERT(isInteractionGuiThread());
+ m_pDefaultStyle->setCameraController(pCameraController);
}
nmVTKInteractionManager::~nmVTKInteractionManager()
@@ -34,11 +38,15 @@ nmVTKInteractionManager::~nmVTKInteractionManager()
// Interactor 持有 Style 引用,必须先清除绑定再释放智能指针。
unbindInteractor();
m_pActiveStyle = nullptr;
+ if(m_pDefaultStyle != nullptr) {
+ m_pDefaultStyle->setCameraController(nullptr);
+ }
m_pDefaultStyle = nullptr;
}
bool nmVTKInteractionManager::bindInteractor(
- vtkRenderWindowInteractor* pInteractor)
+ vtkRenderWindowInteractor* pInteractor,
+ vtkRenderer* pRenderer)
{
Q_ASSERT(isInteractionGuiThread());
if(!isInteractionGuiThread() || pInteractor == nullptr ||
@@ -51,6 +59,8 @@ bool nmVTKInteractionManager::bindInteractor(
unbindInteractor();
m_pInteractor = pInteractor;
}
+ m_pDefaultStyle->SetDefaultRenderer(pRenderer);
+ m_pDefaultStyle->SetCurrentRenderer(pRenderer);
return restoreDefaultInteraction();
}
@@ -101,6 +111,11 @@ void nmVTKInteractionManager::unbindInteractor()
// 本管理器定义单一活动模式边界,解绑时不保留任何旧 Style。
clearInteractionStyle();
+ if(m_pDefaultStyle != nullptr) {
+ // DefaultRenderer 会覆盖 CurrentRenderer,必须先解除前者。
+ m_pDefaultStyle->SetDefaultRenderer(nullptr);
+ m_pDefaultStyle->SetCurrentRenderer(nullptr);
+ }
m_pInteractor = nullptr;
}
@@ -112,10 +127,19 @@ vtkRenderWindowInteractor* nmVTKInteractionManager::getInteractor() const
vtkInteractorStyleTrackballCamera*
nmVTKInteractionManager::getDefaultStyle() const
{
- return m_pDefaultStyle;
+ return m_pDefaultStyle.GetPointer();
}
vtkInteractorStyle* nmVTKInteractionManager::getActiveStyle() const
{
return m_pActiveStyle;
}
+
+bool nmVTKInteractionManager::isDefaultInteractionActive() const
+{
+ vtkInteractorStyle* pDefaultStyle = m_pDefaultStyle.GetPointer();
+ vtkInteractorStyle* pActiveStyle = m_pActiveStyle.GetPointer();
+ return m_pInteractor != nullptr && pDefaultStyle != nullptr &&
+ pActiveStyle == pDefaultStyle &&
+ m_pInteractor->GetInteractorStyle() == pDefaultStyle;
+}
diff --git a/Src/nmNum/nmRender/nmVTKPivotCameraInteractorStyle.cpp b/Src/nmNum/nmRender/nmVTKPivotCameraInteractorStyle.cpp
new file mode 100644
index 0000000..482e30d
--- /dev/null
+++ b/Src/nmNum/nmRender/nmVTKPivotCameraInteractorStyle.cpp
@@ -0,0 +1,295 @@
+#include "nmVTKPivotCameraInteractorStyle.h"
+
+#include "nmVTKCameraController.h"
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+vtkStandardNewMacro(nmVTKPivotCameraInteractorStyle);
+
+namespace {
+
+const double ROTATION_ANGLE_EPSILON = 1.0e-12;
+
+bool isFiniteVector3(const double dVector[3])
+{
+ return dVector != nullptr &&
+ vtkMath::IsFinite(dVector[0]) &&
+ vtkMath::IsFinite(dVector[1]) &&
+ vtkMath::IsFinite(dVector[2]);
+}
+
+bool rotateVector(const double dInput[3],
+ const double dAxis[3],
+ double dAngleDegrees,
+ double dOutput[3])
+{
+ if(!isFiniteVector3(dInput) || !isFiniteVector3(dAxis) ||
+ !vtkMath::IsFinite(dAngleDegrees) || dOutput == nullptr) {
+ return false;
+ }
+
+ if(std::fabs(dAngleDegrees) <= ROTATION_ANGLE_EPSILON) {
+ dOutput[0] = dInput[0];
+ dOutput[1] = dInput[1];
+ dOutput[2] = dInput[2];
+ return true;
+ }
+
+ double dUnitAxis[3] = {
+ dAxis[0], dAxis[1], dAxis[2]
+ };
+ if(vtkMath::Normalize(dUnitAxis) <= 0.0) {
+ return false;
+ }
+
+ const double dAngleRadians =
+ vtkMath::RadiansFromDegrees(dAngleDegrees);
+ const double dCosine = std::cos(dAngleRadians);
+ const double dSine = std::sin(dAngleRadians);
+ const double dProjection = vtkMath::Dot(dUnitAxis, dInput);
+ double dCross[3];
+ vtkMath::Cross(dUnitAxis, dInput, dCross);
+
+ for(int nComponent = 0; nComponent < 3; ++nComponent) {
+ dOutput[nComponent] =
+ dInput[nComponent] * dCosine +
+ dCross[nComponent] * dSine +
+ dUnitAxis[nComponent] * dProjection *
+ (1.0 - dCosine);
+ }
+ return isFiniteVector3(dOutput);
+}
+
+bool rotateCameraFrame(double dPosition[3],
+ double dFocalPoint[3],
+ double dViewUp[3],
+ const double dCenter[3],
+ const double dAxis[3],
+ double dAngleDegrees)
+{
+ if(!isFiniteVector3(dPosition) ||
+ !isFiniteVector3(dFocalPoint) ||
+ !isFiniteVector3(dViewUp) ||
+ !isFiniteVector3(dCenter)) {
+ return false;
+ }
+
+ double dRelativePosition[3];
+ double dRelativeFocalPoint[3];
+ for(int nComponent = 0; nComponent < 3; ++nComponent) {
+ dRelativePosition[nComponent] =
+ dPosition[nComponent] - dCenter[nComponent];
+ dRelativeFocalPoint[nComponent] =
+ dFocalPoint[nComponent] - dCenter[nComponent];
+ }
+
+ double dRotatedPosition[3];
+ double dRotatedFocalPoint[3];
+ double dRotatedViewUp[3];
+ if(!rotateVector(dRelativePosition, dAxis, dAngleDegrees,
+ dRotatedPosition) ||
+ !rotateVector(dRelativeFocalPoint, dAxis, dAngleDegrees,
+ dRotatedFocalPoint) ||
+ !rotateVector(dViewUp, dAxis, dAngleDegrees,
+ dRotatedViewUp)) {
+ return false;
+ }
+
+ for(int nComponent = 0; nComponent < 3; ++nComponent) {
+ dPosition[nComponent] =
+ dCenter[nComponent] + dRotatedPosition[nComponent];
+ dFocalPoint[nComponent] =
+ dCenter[nComponent] + dRotatedFocalPoint[nComponent];
+ dViewUp[nComponent] = dRotatedViewUp[nComponent];
+ }
+ return isFiniteVector3(dPosition) &&
+ isFiniteVector3(dFocalPoint) &&
+ isFiniteVector3(dViewUp);
+}
+
+}
+
+nmVTKPivotCameraInteractorStyle::nmVTKPivotCameraInteractorStyle()
+ : m_pCameraController(nullptr)
+{
+}
+
+nmVTKPivotCameraInteractorStyle::~nmVTKPivotCameraInteractorStyle()
+{
+ m_pCameraController = nullptr;
+}
+
+void nmVTKPivotCameraInteractorStyle::setCameraController(
+ nmVTKCameraController* pController)
+{
+ m_pCameraController = pController;
+}
+
+nmVTKCameraController*
+nmVTKPivotCameraInteractorStyle::getCameraController() const
+{
+ return m_pCameraController;
+}
+
+void nmVTKPivotCameraInteractorStyle::Rotate()
+{
+ double dCenter[3];
+ if(!getCurrentRotationCenter(dCenter)) {
+ vtkInteractorStyleTrackballCamera::Rotate();
+ return;
+ }
+ if(this->CurrentRenderer == nullptr || this->Interactor == nullptr) {
+ return;
+ }
+
+ vtkCamera* pCamera = this->CurrentRenderer->GetActiveCamera();
+ int* pRendererSize = this->CurrentRenderer->GetSize();
+ int* pEventPosition = this->Interactor->GetEventPosition();
+ int* pLastEventPosition = this->Interactor->GetLastEventPosition();
+ if(pCamera == nullptr || pRendererSize == nullptr ||
+ pRendererSize[0] <= 0 || pRendererSize[1] <= 0 ||
+ pEventPosition == nullptr || pLastEventPosition == nullptr) {
+ return;
+ }
+
+ const double dAzimuth =
+ (pEventPosition[0] - pLastEventPosition[0]) *
+ (-20.0 / pRendererSize[0]) * this->MotionFactor;
+ const double dElevation =
+ (pEventPosition[1] - pLastEventPosition[1]) *
+ (-20.0 / pRendererSize[1]) * this->MotionFactor;
+
+ double dPosition[3];
+ double dFocalPoint[3];
+ double dViewUp[3];
+ pCamera->GetPosition(dPosition);
+ pCamera->GetFocalPoint(dFocalPoint);
+ pCamera->GetViewUp(dViewUp);
+
+ if(!rotateCameraFrame(dPosition, dFocalPoint, dViewUp,
+ dCenter, dViewUp, dAzimuth)) {
+ return;
+ }
+
+ // vtkCamera::Elevation 使用 (-DirectionOfProjection) x ViewUp。
+ double dNegativeDirection[3] = {
+ dPosition[0] - dFocalPoint[0],
+ dPosition[1] - dFocalPoint[1],
+ dPosition[2] - dFocalPoint[2]
+ };
+ double dElevationAxis[3];
+ vtkMath::Cross(dNegativeDirection, dViewUp, dElevationAxis);
+ if(!rotateCameraFrame(dPosition, dFocalPoint, dViewUp,
+ dCenter, dElevationAxis, dElevation)) {
+ return;
+ }
+
+ finishCameraMotion(dPosition, dFocalPoint, dViewUp);
+}
+
+void nmVTKPivotCameraInteractorStyle::Spin()
+{
+ double dCenter[3];
+ if(!getCurrentRotationCenter(dCenter)) {
+ vtkInteractorStyleTrackballCamera::Spin();
+ return;
+ }
+ if(this->CurrentRenderer == nullptr || this->Interactor == nullptr) {
+ return;
+ }
+
+ vtkCamera* pCamera = this->CurrentRenderer->GetActiveCamera();
+ int* pEventPosition = this->Interactor->GetEventPosition();
+ int* pLastEventPosition = this->Interactor->GetLastEventPosition();
+ if(pCamera == nullptr || pEventPosition == nullptr ||
+ pLastEventPosition == nullptr) {
+ return;
+ }
+
+ this->CurrentRenderer->SetWorldPoint(
+ dCenter[0], dCenter[1], dCenter[2], 1.0);
+ this->CurrentRenderer->WorldToDisplay();
+ const double* pDisplayCenter =
+ this->CurrentRenderer->GetDisplayPoint();
+ if(!isFiniteVector3(pDisplayCenter)) {
+ return;
+ }
+
+ const double dNewAngle = std::atan2(
+ pEventPosition[1] - pDisplayCenter[1],
+ pEventPosition[0] - pDisplayCenter[0]);
+ const double dOldAngle = std::atan2(
+ pLastEventPosition[1] - pDisplayCenter[1],
+ pLastEventPosition[0] - pDisplayCenter[0]);
+ double dSpinAngle = vtkMath::DegreesFromRadians(
+ dNewAngle - dOldAngle);
+ if(dSpinAngle > 180.0) {
+ dSpinAngle -= 360.0;
+ } else if(dSpinAngle < -180.0) {
+ dSpinAngle += 360.0;
+ }
+
+ double dPosition[3];
+ double dFocalPoint[3];
+ double dViewUp[3];
+ pCamera->GetPosition(dPosition);
+ pCamera->GetFocalPoint(dFocalPoint);
+ pCamera->GetViewUp(dViewUp);
+ double dDirectionOfProjection[3] = {
+ dFocalPoint[0] - dPosition[0],
+ dFocalPoint[1] - dPosition[1],
+ dFocalPoint[2] - dPosition[2]
+ };
+ if(!rotateCameraFrame(dPosition, dFocalPoint, dViewUp,
+ dCenter, dDirectionOfProjection,
+ dSpinAngle)) {
+ return;
+ }
+
+ finishCameraMotion(dPosition, dFocalPoint, dViewUp);
+}
+
+bool nmVTKPivotCameraInteractorStyle::getCurrentRotationCenter(
+ double dCenter[3]) const
+{
+ return dCenter != nullptr && m_pCameraController != nullptr &&
+ this->CurrentRenderer != nullptr &&
+ m_pCameraController->getRenderer() == this->CurrentRenderer &&
+ m_pCameraController->getRotationCenter(dCenter);
+}
+
+void nmVTKPivotCameraInteractorStyle::finishCameraMotion(
+ const double dPosition[3],
+ const double dFocalPoint[3],
+ const double dViewUp[3])
+{
+ if(this->CurrentRenderer == nullptr || this->Interactor == nullptr ||
+ !isFiniteVector3(dPosition) ||
+ !isFiniteVector3(dFocalPoint) ||
+ !isFiniteVector3(dViewUp)) {
+ return;
+ }
+
+ vtkCamera* pCamera = this->CurrentRenderer->GetActiveCamera();
+ if(pCamera == nullptr) {
+ return;
+ }
+ pCamera->SetPosition(dPosition);
+ pCamera->SetFocalPoint(dFocalPoint);
+ pCamera->SetViewUp(dViewUp);
+ pCamera->OrthogonalizeViewUp();
+
+ if(this->AutoAdjustCameraClippingRange != 0) {
+ this->CurrentRenderer->ResetCameraClippingRange();
+ }
+ if(this->Interactor->GetLightFollowCamera() != 0) {
+ this->CurrentRenderer->UpdateLightsGeometryToFollowCamera();
+ }
+ this->Interactor->Render();
+}
diff --git a/Src/nmNum/nmRender/nmVTKRotationCenterMarkerLayer.cpp b/Src/nmNum/nmRender/nmVTKRotationCenterMarkerLayer.cpp
new file mode 100644
index 0000000..f24b5fd
--- /dev/null
+++ b/Src/nmNum/nmRender/nmVTKRotationCenterMarkerLayer.cpp
@@ -0,0 +1,530 @@
+#include "nmVTKRotationCenterMarkerLayer.h"
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+namespace {
+
+const double ROTATION_CENTER_HALF_LENGTH_PIXELS = 18.0;
+const double ROTATION_CENTER_DEPTH_OFFSET_PIXELS = 1.5;
+const double ROTATION_CENTER_HOMOGENEOUS_EPSILON =
+ std::numeric_limits::epsilon();
+
+bool isRotationCenterMarkerGuiThread()
+{
+ QCoreApplication* pApplication = QCoreApplication::instance();
+ return pApplication == nullptr ||
+ pApplication->thread() == QThread::currentThread();
+}
+
+bool isFinitePoint3(const double dPoint[3])
+{
+ return dPoint != nullptr &&
+ vtkMath::IsFinite(dPoint[0]) &&
+ vtkMath::IsFinite(dPoint[1]) &&
+ vtkMath::IsFinite(dPoint[2]);
+}
+
+bool worldToDisplay(vtkRenderer* pRenderer,
+ const double dWorld[3],
+ double dDisplay[3])
+{
+ if(pRenderer == nullptr || dWorld == nullptr || dDisplay == nullptr) {
+ return false;
+ }
+
+ pRenderer->SetWorldPoint(dWorld[0], dWorld[1], dWorld[2], 1.0);
+ pRenderer->WorldToDisplay();
+ const double* pDisplay = pRenderer->GetDisplayPoint();
+ if(!isFinitePoint3(pDisplay)) {
+ return false;
+ }
+
+ dDisplay[0] = pDisplay[0];
+ dDisplay[1] = pDisplay[1];
+ dDisplay[2] = pDisplay[2];
+ return true;
+}
+
+bool displayToWorld(vtkRenderer* pRenderer,
+ const double dDisplay[3],
+ double dWorld[3])
+{
+ if(pRenderer == nullptr || dDisplay == nullptr || dWorld == nullptr ||
+ !isFinitePoint3(dDisplay)) {
+ return false;
+ }
+
+ pRenderer->SetDisplayPoint(
+ dDisplay[0], dDisplay[1], dDisplay[2]);
+ pRenderer->DisplayToWorld();
+ const double* pHomogeneousWorld = pRenderer->GetWorldPoint();
+ if(pHomogeneousWorld == nullptr ||
+ !vtkMath::IsFinite(pHomogeneousWorld[0]) ||
+ !vtkMath::IsFinite(pHomogeneousWorld[1]) ||
+ !vtkMath::IsFinite(pHomogeneousWorld[2]) ||
+ !vtkMath::IsFinite(pHomogeneousWorld[3]) ||
+ std::fabs(pHomogeneousWorld[3]) <=
+ ROTATION_CENTER_HOMOGENEOUS_EPSILON) {
+ return false;
+ }
+
+ const double dInverseW = 1.0 / pHomogeneousWorld[3];
+ dWorld[0] = pHomogeneousWorld[0] * dInverseW;
+ dWorld[1] = pHomogeneousWorld[1] * dInverseW;
+ dWorld[2] = pHomogeneousWorld[2] * dInverseW;
+ return isFinitePoint3(dWorld);
+}
+
+}
+
+class nmVTKRotationCenterMarkerLayerPrivate
+{
+public:
+ nmVTKRotationCenterMarkerLayerPrivate()
+ : m_pPoints(vtkSmartPointer::New()),
+ m_pLines(vtkSmartPointer::New()),
+ m_pColors(vtkSmartPointer::New()),
+ m_pPolyData(vtkSmartPointer::New()),
+ m_pMapper(vtkSmartPointer::New()),
+ m_pActor(vtkSmartPointer::New()),
+ m_pRendererCallback(vtkSmartPointer::New()),
+ m_pCameraCallback(vtkSmartPointer::New()),
+ m_pRenderer(nullptr),
+ m_pCamera(nullptr),
+ m_nRendererObserverTag(0),
+ m_nCameraObserverTag(0),
+ m_bHasWorldCenter(false)
+ {
+ // 必须通过 vtkActor::New() 的对象工厂路径获得实际 OpenGL Actor。
+ initializeGeometry();
+
+ m_dWorldCenter[0] = 0.0;
+ m_dWorldCenter[1] = 0.0;
+ m_dWorldCenter[2] = 0.0;
+
+ m_pRendererCallback->SetClientData(this);
+ m_pRendererCallback->SetCallback(
+ nmVTKRotationCenterMarkerLayerPrivate::rendererStarted);
+ m_pCameraCallback->SetClientData(this);
+ m_pCameraCallback->SetCallback(
+ nmVTKRotationCenterMarkerLayerPrivate::cameraModified);
+ }
+
+ ~nmVTKRotationCenterMarkerLayerPrivate()
+ {
+ // 先移除 Renderer 和 Camera Observer,再阻断裸回调目标。
+ unbindRenderer();
+ m_pRendererCallback->SetClientData(nullptr);
+ m_pRendererCallback->SetCallback(nullptr);
+ m_pCameraCallback->SetClientData(nullptr);
+ m_pCameraCallback->SetCallback(nullptr);
+ }
+
+ vtkActor* getActor() const
+ {
+ return m_pActor;
+ }
+
+ bool bindRenderer(vtkRenderer* pRenderer)
+ {
+ if(pRenderer == nullptr) {
+ return false;
+ }
+
+ if(m_pRenderer.GetPointer() != pRenderer) {
+ unbindRenderer();
+ m_pRenderer = pRenderer;
+ }
+
+ if(!ensureRendererObserver(pRenderer) ||
+ !ensureCameraObserver(pRenderer->GetActiveCamera())) {
+ unbindRenderer();
+ return false;
+ }
+ return true;
+ }
+
+ void unbindRenderer()
+ {
+ removeCameraObserver();
+ removeRendererObserver();
+ m_pRenderer = nullptr;
+ }
+
+ bool synchronize()
+ {
+ return synchronizeForRenderer(m_pRenderer.GetPointer());
+ }
+
+ bool synchronizeForRenderer(vtkRenderer* pRenderer)
+ {
+ if(pRenderer == nullptr || m_pRenderer.GetPointer() != pRenderer ||
+ !m_bHasWorldCenter) {
+ return false;
+ }
+
+ vtkCamera* pCamera = pRenderer->GetActiveCamera();
+ if(pCamera == nullptr || !ensureCameraObserver(pCamera)) {
+ return false;
+ }
+
+ int* pViewportSize = pRenderer->GetSize();
+ if(pViewportSize == nullptr || pViewportSize[0] <= 0 ||
+ pViewportSize[1] <= 0) {
+ return false;
+ }
+
+ const double dLogicalCenter[3] = {
+ m_dWorldCenter[0], m_dWorldCenter[1], m_dWorldCenter[2]
+ };
+ double dCameraPosition[3];
+ pCamera->GetPosition(dCameraPosition);
+ if(!isFinitePoint3(dLogicalCenter) ||
+ !isFinitePoint3(dCameraPosition)) {
+ return false;
+ }
+
+ double dCenterDisplay[3];
+ if(!worldToDisplay(pRenderer, dLogicalCenter, dCenterDisplay)) {
+ return false;
+ }
+
+ double dOffsetDisplay[3] = {
+ dCenterDisplay[0],
+ dCenterDisplay[1] + ROTATION_CENTER_HALF_LENGTH_PIXELS,
+ dCenterDisplay[2]
+ };
+ double dCenterAtDepth[3];
+ double dOffsetAtDepth[3];
+ if(!displayToWorld(pRenderer, dCenterDisplay, dCenterAtDepth) ||
+ !displayToWorld(pRenderer, dOffsetDisplay, dOffsetAtDepth)) {
+ return false;
+ }
+
+ const double dScaleX = dOffsetAtDepth[0] - dCenterAtDepth[0];
+ const double dScaleY = dOffsetAtDepth[1] - dCenterAtDepth[1];
+ const double dScaleZ = dOffsetAtDepth[2] - dCenterAtDepth[2];
+ const double dHalfLength = std::sqrt(
+ dScaleX * dScaleX +
+ dScaleY * dScaleY +
+ dScaleZ * dScaleZ);
+ if(!vtkMath::IsFinite(dHalfLength) || dHalfLength <= 0.0) {
+ return false;
+ }
+
+ double dTowardCamera[3];
+ if(pCamera->GetParallelProjection() != 0) {
+ const double* pDirectionOfProjection =
+ pCamera->GetDirectionOfProjection();
+ if(!isFinitePoint3(pDirectionOfProjection)) {
+ return false;
+ }
+ dTowardCamera[0] = -pDirectionOfProjection[0];
+ dTowardCamera[1] = -pDirectionOfProjection[1];
+ dTowardCamera[2] = -pDirectionOfProjection[2];
+ } else {
+ dTowardCamera[0] = dCameraPosition[0] - dLogicalCenter[0];
+ dTowardCamera[1] = dCameraPosition[1] - dLogicalCenter[1];
+ dTowardCamera[2] = dCameraPosition[2] - dLogicalCenter[2];
+ }
+ if(vtkMath::Normalize(dTowardCamera) <= 0.0) {
+ return false;
+ }
+
+ const double dWorldPerPixel =
+ dHalfLength / ROTATION_CENTER_HALF_LENGTH_PIXELS;
+ const double dDepthOffset =
+ dWorldPerPixel * ROTATION_CENTER_DEPTH_OFFSET_PIXELS;
+ const double dDisplayCenter[3] = {
+ dLogicalCenter[0] + dTowardCamera[0] * dDepthOffset,
+ dLogicalCenter[1] + dTowardCamera[1] * dDepthOffset,
+ dLogicalCenter[2] + dTowardCamera[2] * dDepthOffset
+ };
+
+ const double dAxisPoints[6][3] = {
+ { dDisplayCenter[0] - dHalfLength,
+ dDisplayCenter[1], dDisplayCenter[2] },
+ { dDisplayCenter[0] + dHalfLength,
+ dDisplayCenter[1], dDisplayCenter[2] },
+ { dDisplayCenter[0],
+ dDisplayCenter[1] - dHalfLength, dDisplayCenter[2] },
+ { dDisplayCenter[0],
+ dDisplayCenter[1] + dHalfLength, dDisplayCenter[2] },
+ { dDisplayCenter[0], dDisplayCenter[1],
+ dDisplayCenter[2] - dHalfLength },
+ { dDisplayCenter[0], dDisplayCenter[1],
+ dDisplayCenter[2] + dHalfLength }
+ };
+ for(vtkIdType nPoint = 0; nPoint < 6; ++nPoint) {
+ if(!isFinitePoint3(dAxisPoints[nPoint])) {
+ return false;
+ }
+ }
+ for(vtkIdType nPoint = 0; nPoint < 6; ++nPoint) {
+ m_pPoints->SetPoint(nPoint, dAxisPoints[nPoint]);
+ }
+ m_pPoints->Modified();
+ m_pPolyData->Modified();
+ return true;
+ }
+
+ bool setWorldCenter(double dX, double dY, double dZ)
+ {
+ const double dCenter[3] = { dX, dY, dZ };
+ if(!isFinitePoint3(dCenter)) {
+ return false;
+ }
+ m_dWorldCenter[0] = dX;
+ m_dWorldCenter[1] = dY;
+ m_dWorldCenter[2] = dZ;
+ m_bHasWorldCenter = true;
+ synchronize();
+ return true;
+ }
+
+ bool hasWorldCenter() const
+ {
+ return m_bHasWorldCenter;
+ }
+
+private:
+ static void rendererStarted(vtkObject* pCaller,
+ unsigned long,
+ void* pClientData,
+ void*)
+ {
+ nmVTKRotationCenterMarkerLayerPrivate* pThis =
+ static_cast(
+ pClientData);
+ if(pThis != nullptr) {
+ // StartEvent 位于 Prop 遍历之前,可使用最终视口尺寸覆盖 Resize 路径。
+ pThis->synchronizeForRenderer(
+ vtkRenderer::SafeDownCast(pCaller));
+ }
+ }
+
+ static void cameraModified(vtkObject*,
+ unsigned long,
+ void* pClientData,
+ void*)
+ {
+ nmVTKRotationCenterMarkerLayerPrivate* pThis =
+ static_cast(
+ pClientData);
+ if(pThis != nullptr) {
+ // 相机回调只同步几何,最终 Render 仍由交互器或容器统一提交。
+ pThis->synchronize();
+ }
+ }
+
+ void initializeGeometry()
+ {
+ m_pPoints->SetDataTypeToDouble();
+ m_pPoints->SetNumberOfPoints(6);
+ for(vtkIdType nPoint = 0; nPoint < 6; ++nPoint) {
+ m_pPoints->SetPoint(nPoint, 0.0, 0.0, 0.0);
+ }
+
+ for(vtkIdType nAxis = 0; nAxis < 3; ++nAxis) {
+ vtkIdType nPointIds[2] = {
+ nAxis * 2, nAxis * 2 + 1
+ };
+ m_pLines->InsertNextCell(2, nPointIds);
+ }
+
+ m_pColors->SetName("RotationCenterAxisColors");
+ m_pColors->SetNumberOfComponents(3);
+ m_pColors->InsertNextTuple3(255.0, 0.0, 0.0);
+ m_pColors->InsertNextTuple3(254.0, 255.0, 0.0);
+ m_pColors->InsertNextTuple3(0.0, 255.0, 0.0);
+
+ m_pPolyData->SetPoints(m_pPoints);
+ m_pPolyData->SetLines(m_pLines);
+ m_pPolyData->GetCellData()->SetScalars(m_pColors);
+
+ m_pMapper->SetInputData(m_pPolyData);
+ m_pMapper->ScalarVisibilityOn();
+ m_pMapper->SetScalarModeToUseCellData();
+ m_pMapper->SetColorModeToDirectScalars();
+
+ m_pActor->SetMapper(m_pMapper);
+ m_pActor->GetProperty()->SetLineWidth(2.0f);
+ m_pActor->GetProperty()->SetOpacity(1.0);
+ m_pActor->GetProperty()->LightingOff();
+ m_pActor->PickableOff();
+ m_pActor->UseBoundsOff();
+ }
+
+ bool ensureRendererObserver(vtkRenderer* pRenderer)
+ {
+ if(pRenderer == nullptr || m_pRendererCallback == nullptr) {
+ return false;
+ }
+ if(m_pRenderer.GetPointer() == pRenderer &&
+ m_nRendererObserverTag != 0) {
+ return true;
+ }
+
+ removeRendererObserver();
+ m_nRendererObserverTag = pRenderer->AddObserver(
+ vtkCommand::StartEvent, m_pRendererCallback);
+ return m_nRendererObserverTag != 0;
+ }
+
+ void removeRendererObserver()
+ {
+ vtkRenderer* pRenderer = m_pRenderer.GetPointer();
+ if(pRenderer != nullptr && m_nRendererObserverTag != 0) {
+ pRenderer->RemoveObserver(m_nRendererObserverTag);
+ }
+ m_nRendererObserverTag = 0;
+ }
+
+ bool ensureCameraObserver(vtkCamera* pCamera)
+ {
+ if(pCamera == nullptr || m_pCameraCallback == nullptr) {
+ return false;
+ }
+ if(m_pCamera.GetPointer() == pCamera &&
+ m_nCameraObserverTag != 0) {
+ return true;
+ }
+
+ removeCameraObserver();
+ m_pCamera = pCamera;
+ m_nCameraObserverTag = pCamera->AddObserver(
+ vtkCommand::ModifiedEvent, m_pCameraCallback);
+ if(m_nCameraObserverTag == 0) {
+ m_pCamera = nullptr;
+ return false;
+ }
+ return true;
+ }
+
+ void removeCameraObserver()
+ {
+ vtkCamera* pCamera = m_pCamera.GetPointer();
+ if(pCamera != nullptr && m_nCameraObserverTag != 0) {
+ pCamera->RemoveObserver(m_nCameraObserverTag);
+ }
+ m_nCameraObserverTag = 0;
+ m_pCamera = nullptr;
+ }
+
+ vtkSmartPointer m_pPoints;
+ vtkSmartPointer m_pLines;
+ vtkSmartPointer m_pColors;
+ vtkSmartPointer m_pPolyData;
+ vtkSmartPointer m_pMapper;
+ vtkSmartPointer m_pActor;
+ vtkSmartPointer m_pRendererCallback;
+ vtkSmartPointer m_pCameraCallback;
+ vtkWeakPointer m_pRenderer; ///< 非所有权引用。
+ vtkWeakPointer m_pCamera; ///< 非所有权引用。
+ unsigned long m_nRendererObserverTag; ///< 为零表示尚未注册观察者。
+ unsigned long m_nCameraObserverTag; ///< 为零表示尚未注册观察者。
+ double m_dWorldCenter[3]; ///< 独立于相机焦点的逻辑中心。
+ bool m_bHasWorldCenter;
+};
+
+nmVTKRotationCenterMarkerLayer::nmVTKRotationCenterMarkerLayer(
+ const QString& sLayerId)
+ : nmVTKRenderLayer(sLayerId),
+ m_pPrivate(new nmVTKRotationCenterMarkerLayerPrivate)
+{
+ Q_ASSERT(isRotationCenterMarkerGuiThread());
+ setRenderProp(m_pPrivate->getActor());
+ setVisible(false);
+}
+
+nmVTKRotationCenterMarkerLayer::~nmVTKRotationCenterMarkerLayer()
+{
+ Q_ASSERT(isRotationCenterMarkerGuiThread());
+ if(m_pPrivate != nullptr) {
+ // Renderer/Camera Observer 必须在 Renderer 和回调目标释放前解除。
+ m_pPrivate->unbindRenderer();
+ detach();
+ delete m_pPrivate;
+ m_pPrivate = nullptr;
+ }
+}
+
+bool nmVTKRotationCenterMarkerLayer::bindRenderer(vtkRenderer* pRenderer)
+{
+ Q_ASSERT(isRotationCenterMarkerGuiThread());
+ if(!isRotationCenterMarkerGuiThread() || m_pPrivate == nullptr ||
+ pRenderer == nullptr || getAttachedRenderer() != pRenderer) {
+ return false;
+ }
+ return m_pPrivate->bindRenderer(pRenderer);
+}
+
+void nmVTKRotationCenterMarkerLayer::unbindRenderer()
+{
+ Q_ASSERT(isRotationCenterMarkerGuiThread());
+ if(isRotationCenterMarkerGuiThread() && m_pPrivate != nullptr) {
+ m_pPrivate->unbindRenderer();
+ }
+}
+
+bool nmVTKRotationCenterMarkerLayer::synchronize()
+{
+ Q_ASSERT(isRotationCenterMarkerGuiThread());
+ return isRotationCenterMarkerGuiThread() && m_pPrivate != nullptr &&
+ m_pPrivate->synchronize();
+}
+
+bool nmVTKRotationCenterMarkerLayer::setWorldCenter(
+ double dX,
+ double dY,
+ double dZ)
+{
+ Q_ASSERT(isRotationCenterMarkerGuiThread());
+ return isRotationCenterMarkerGuiThread() && m_pPrivate != nullptr &&
+ m_pPrivate->setWorldCenter(dX, dY, dZ);
+}
+
+bool nmVTKRotationCenterMarkerLayer::hasWorldCenter() const
+{
+ return m_pPrivate != nullptr && m_pPrivate->hasWorldCenter();
+}
+
+void nmVTKRotationCenterMarkerLayer::setMarkerVisible(bool bVisible)
+{
+ Q_ASSERT(isRotationCenterMarkerGuiThread());
+ if(!isRotationCenterMarkerGuiThread()) {
+ return;
+ }
+
+ const bool bCanShow = bVisible && m_pPrivate != nullptr &&
+ m_pPrivate->hasWorldCenter();
+ setVisible(bCanShow);
+ if(bCanShow) {
+ // 首次显示先准备几何;视口尚未建立时由绘制前同步再次补齐。
+ m_pPrivate->synchronize();
+ }
+}
+
+bool nmVTKRotationCenterMarkerLayer::isMarkerVisible() const
+{
+ return isVisible();
+}
diff --git a/Src/nmNum/nmRender/nmVTKRotationCenterPicker.cpp b/Src/nmNum/nmRender/nmVTKRotationCenterPicker.cpp
new file mode 100644
index 0000000..76198b6
--- /dev/null
+++ b/Src/nmNum/nmRender/nmVTKRotationCenterPicker.cpp
@@ -0,0 +1,316 @@
+#include "nmVTKRotationCenterPicker.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace {
+
+// Qt 控件状态和 VTK Picker 均只允许在 GUI 线程中修改。
+bool isRotationCenterPickerGuiThread()
+{
+ QCoreApplication* pApplication = QCoreApplication::instance();
+ return pApplication == nullptr ||
+ pApplication->thread() == QThread::currentThread();
+}
+
+int scalePixelCoordinate(int nPosition,
+ int nWidgetExtent,
+ int nRenderExtent)
+{
+ const double dScaled =
+ (static_cast(nPosition) + 0.5) *
+ static_cast(nRenderExtent) /
+ static_cast(nWidgetExtent) - 0.5;
+ int nResult = static_cast(dScaled + 0.5);
+ if(nResult < 0) {
+ nResult = 0;
+ } else if(nResult >= nRenderExtent) {
+ nResult = nRenderExtent - 1;
+ }
+ return nResult;
+}
+
+}
+
+nmVTKRotationCenterPicker::nmVTKRotationCenterPicker()
+ : m_pPickTarget(nullptr),
+ m_pWidget(nullptr),
+ m_pRenderer(nullptr),
+ m_oPreviousCursor(),
+ m_nPendingReleaseButton(static_cast(Qt::NoButton)),
+ m_bHadExplicitCursor(false),
+ m_bCursorOverridden(false),
+ m_bActive(false),
+ m_bHasPickedPosition(false)
+{
+ Q_ASSERT(isRotationCenterPickerGuiThread());
+ m_dPickedPosition[0] = 0.0;
+ m_dPickedPosition[1] = 0.0;
+ m_dPickedPosition[2] = 0.0;
+}
+
+nmVTKRotationCenterPicker::~nmVTKRotationCenterPicker()
+{
+ Q_ASSERT(isRotationCenterPickerGuiThread());
+ // 先恢复 Qt 临时状态,再解除对拾取目标的弱引用观察关系。
+ reset();
+ m_pPickTarget = nullptr;
+}
+
+void nmVTKRotationCenterPicker::setPickTarget(vtkProp* pTarget)
+{
+ Q_ASSERT(isRotationCenterPickerGuiThread());
+ if(!isRotationCenterPickerGuiThread()) {
+ return;
+ }
+
+ if(m_pPickTarget.GetPointer() == pTarget) {
+ // 弱目标已经失效时,显式清空仍必须结束可能遗留的交互状态。
+ if(pTarget == nullptr && (m_bActive || hasPendingRelease())) {
+ reset();
+ }
+ return;
+ }
+
+ // 网格更换时旧 Press/Release 序列不再有效,必须先结束当前模式。
+ reset();
+ m_pPickTarget = pTarget;
+}
+
+vtkProp* nmVTKRotationCenterPicker::getPickTarget() const
+{
+ return m_pPickTarget.GetPointer();
+}
+
+bool nmVTKRotationCenterPicker::hasPickTarget() const
+{
+ return m_pPickTarget.GetPointer() != nullptr;
+}
+
+bool nmVTKRotationCenterPicker::start(
+ QVTKWidget* pWidget,
+ vtkRenderer* pRenderer)
+{
+ Q_ASSERT(isRotationCenterPickerGuiThread());
+ if(!isRotationCenterPickerGuiThread() || m_bActive ||
+ hasPendingRelease() || pWidget == nullptr || pRenderer == nullptr ||
+ !hasPickTarget()) {
+ return false;
+ }
+
+ vtkRenderWindow* pRenderWindow = pRenderer->GetRenderWindow();
+ if(pRenderWindow == nullptr ||
+ pWidget->GetRenderWindow() != pRenderWindow) {
+ return false;
+ }
+
+ m_pWidget = pWidget;
+ m_pRenderer = pRenderer;
+ m_bHasPickedPosition = false;
+ m_bHadExplicitCursor = pWidget->testAttribute(Qt::WA_SetCursor);
+ m_oPreviousCursor = pWidget->cursor();
+ pWidget->setCursor(Qt::CrossCursor);
+ m_bCursorOverridden = true;
+ m_bActive = true;
+ pWidget->setFocus(Qt::OtherFocusReason);
+ return true;
+}
+
+nmVTKRotationCenterPicker::PickEventResult
+nmVTKRotationCenterPicker::cancel()
+{
+ const bool bWasActive = m_bActive;
+ reset();
+ return PickEventResult(false,
+ bWasActive ? PickOutcome_Cancelled :
+ PickOutcome_None);
+}
+
+void nmVTKRotationCenterPicker::reset()
+{
+ Q_ASSERT(isRotationCenterPickerGuiThread());
+ if(!isRotationCenterPickerGuiThread()) {
+ return;
+ }
+
+ m_bActive = false;
+ m_nPendingReleaseButton = static_cast(Qt::NoButton);
+ restoreCursor();
+ m_pWidget = nullptr;
+ m_pRenderer = nullptr;
+ m_bHasPickedPosition = false;
+}
+
+bool nmVTKRotationCenterPicker::isActive() const
+{
+ return m_bActive;
+}
+
+bool nmVTKRotationCenterPicker::hasPendingRelease() const
+{
+ return m_nPendingReleaseButton != static_cast(Qt::NoButton);
+}
+
+nmVTKRotationCenterPicker::PickEventResult
+nmVTKRotationCenterPicker::handleEvent(QEvent* pEvent)
+{
+ Q_ASSERT(isRotationCenterPickerGuiThread());
+ if(!isRotationCenterPickerGuiThread() || pEvent == nullptr) {
+ return PickEventResult();
+ }
+
+ if(pEvent->type() == QEvent::MouseButtonRelease &&
+ hasPendingRelease()) {
+ QMouseEvent* pMouseEvent = static_cast(pEvent);
+ if(static_cast(pMouseEvent->button()) ==
+ m_nPendingReleaseButton) {
+ m_nPendingReleaseButton = static_cast(Qt::NoButton);
+ return PickEventResult(true, PickOutcome_None);
+ }
+ return PickEventResult();
+ }
+
+ if(!m_bActive) {
+ return PickEventResult();
+ }
+
+ if(pEvent->type() == QEvent::ShortcutOverride ||
+ pEvent->type() == QEvent::KeyPress) {
+ QKeyEvent* pKeyEvent = static_cast(pEvent);
+ if(pKeyEvent->key() == Qt::Key_Escape) {
+ if(pEvent->type() == QEvent::ShortcutOverride) {
+ pEvent->accept();
+ return PickEventResult(true, PickOutcome_None);
+ }
+ finishActiveMode();
+ return PickEventResult(true, PickOutcome_Cancelled);
+ }
+ }
+
+ if(pEvent->type() != QEvent::MouseButtonPress) {
+ return PickEventResult();
+ }
+
+ QMouseEvent* pMouseEvent = static_cast(pEvent);
+ if(pMouseEvent->button() != Qt::LeftButton &&
+ pMouseEvent->button() != Qt::RightButton) {
+ return PickEventResult();
+ }
+
+ m_nPendingReleaseButton = static_cast(pMouseEvent->button());
+ if(pMouseEvent->button() == Qt::RightButton) {
+ finishActiveMode();
+ return PickEventResult(true, PickOutcome_Cancelled);
+ }
+
+ const bool bHit = pickAt(pMouseEvent);
+ finishActiveMode();
+ return PickEventResult(true,
+ bHit ? PickOutcome_Hit : PickOutcome_Miss);
+}
+
+bool nmVTKRotationCenterPicker::getPickedPosition(
+ double dPosition[3]) const
+{
+ if(dPosition == nullptr || !m_bHasPickedPosition) {
+ return false;
+ }
+ dPosition[0] = m_dPickedPosition[0];
+ dPosition[1] = m_dPickedPosition[1];
+ dPosition[2] = m_dPickedPosition[2];
+ return true;
+}
+
+bool nmVTKRotationCenterPicker::pickAt(
+ const QMouseEvent* pMouseEvent)
+{
+ m_bHasPickedPosition = false;
+ if(pMouseEvent == nullptr || m_pWidget == nullptr ||
+ m_pRenderer == nullptr) {
+ return false;
+ }
+
+ vtkProp* pTarget = m_pPickTarget.GetPointer();
+ vtkRenderWindow* pRenderWindow = m_pRenderer->GetRenderWindow();
+ if(pTarget == nullptr || pRenderWindow == nullptr ||
+ m_pWidget->GetRenderWindow() != pRenderWindow) {
+ return false;
+ }
+
+ const int nWidgetWidth = m_pWidget->width();
+ const int nWidgetHeight = m_pWidget->height();
+ int* pRenderSize = pRenderWindow->GetActualSize();
+ if(nWidgetWidth <= 0 || nWidgetHeight <= 0 || pRenderSize == nullptr ||
+ pRenderSize[0] <= 0 || pRenderSize[1] <= 0 ||
+ pMouseEvent->x() < 0 || pMouseEvent->x() >= nWidgetWidth ||
+ pMouseEvent->y() < 0 || pMouseEvent->y() >= nWidgetHeight) {
+ return false;
+ }
+
+ const int nRenderX = scalePixelCoordinate(
+ pMouseEvent->x(), nWidgetWidth, pRenderSize[0]);
+ const int nRenderYFromTop = scalePixelCoordinate(
+ pMouseEvent->y(), nWidgetHeight, pRenderSize[1]);
+ const int nRenderY = pRenderSize[1] - 1 - nRenderYFromTop;
+
+ // Picker 也限定在本次调用内,避免命中结果缓存延长 Mesh Actor 生命周期。
+ vtkSmartPointer pCellPicker =
+ vtkSmartPointer::New();
+ if(pCellPicker == nullptr) {
+ return false;
+ }
+ pCellPicker->PickFromListOn();
+ pCellPicker->InitializePickList();
+ pCellPicker->AddPickList(pTarget);
+ const int nPicked = pCellPicker->Pick(
+ nRenderX, nRenderY, 0.0, m_pRenderer);
+ const vtkIdType nCellId = pCellPicker->GetCellId();
+ double* pPosition = pCellPicker->GetPickPosition();
+ const bool bValid = nPicked != 0 && nCellId >= 0 &&
+ pPosition != nullptr &&
+ vtkMath::IsFinite(pPosition[0]) &&
+ vtkMath::IsFinite(pPosition[1]) &&
+ vtkMath::IsFinite(pPosition[2]);
+ if(bValid) {
+ m_dPickedPosition[0] = pPosition[0];
+ m_dPickedPosition[1] = pPosition[1];
+ m_dPickedPosition[2] = pPosition[2];
+ m_bHasPickedPosition = true;
+ }
+ pCellPicker->InitializePickList();
+ return bValid;
+}
+
+void nmVTKRotationCenterPicker::finishActiveMode()
+{
+ m_bActive = false;
+ restoreCursor();
+ m_pWidget = nullptr;
+ m_pRenderer = nullptr;
+}
+
+void nmVTKRotationCenterPicker::restoreCursor()
+{
+ if(!m_bCursorOverridden || m_pWidget == nullptr) {
+ m_bCursorOverridden = false;
+ return;
+ }
+
+ if(m_bHadExplicitCursor) {
+ m_pWidget->setCursor(m_oPreviousCursor);
+ } else {
+ m_pWidget->unsetCursor();
+ }
+ m_bCursorOverridden = false;
+}
diff --git a/Src/nmNum/nmRender/nmVTKViewCommandController.cpp b/Src/nmNum/nmRender/nmVTKViewCommandController.cpp
index 7b3b671..0b7b53e 100644
--- a/Src/nmNum/nmRender/nmVTKViewCommandController.cpp
+++ b/Src/nmNum/nmRender/nmVTKViewCommandController.cpp
@@ -27,7 +27,8 @@ nmVTKViewCommandController::nmVTKViewCommandController(
: QObject(pParent),
m_pContainer(pContainer),
m_eCapabilities(0),
- m_bViewAvailable(false)
+ m_bViewAvailable(false),
+ m_bRotationCenterPickingAvailable(false)
{
for(int nIndex = 0; nIndex < ViewCommandCount; ++nIndex) {
m_pActions[nIndex] = nullptr;
@@ -51,6 +52,17 @@ nmVTKViewCommandController::nmVTKViewCommandController(
createAction(ViewCommand_ResetRotationCenter,
tr("Reset Rotation Center"), QString(),
SLOT(resetRotationCenter()));
+ QAction* pSetRotationCenterAction = createAction(
+ ViewCommand_SetRotationCenter,
+ tr("Set Rotation Center"), "NmSetCenter.png", nullptr);
+ if(pSetRotationCenterAction != nullptr) {
+ pSetRotationCenterAction->setCheckable(true);
+ if(m_pContainer != nullptr) {
+ connect(pSetRotationCenterAction, SIGNAL(toggled(bool)),
+ m_pContainer,
+ SLOT(setRotationCenterPickingEnabled(bool)));
+ }
+ }
createAction(ViewCommand_SaveImage, tr("Save as Image"), "SaveImg.png",
SLOT(saveImage()));
createAction(ViewCommand_CopyImage, tr("Copy Image"),
@@ -101,10 +113,13 @@ void nmVTKViewCommandController::populateToolBar(QToolBar* pToolBar) const
ViewCommand_Front,
ViewCommand_Right,
ViewCommand_Isometric,
- ViewCommand_ResetRotationCenter
+ ViewCommand_ResetRotationCenter,
+ ViewCommand_SetRotationCenter
};
bool bHasNavigation = false;
- for(int nIndex = 0; nIndex < 8; ++nIndex) {
+ const int nNavigationCommandCount =
+ sizeof(eNavigationCommands) / sizeof(eNavigationCommands[0]);
+ for(int nIndex = 0; nIndex < nNavigationCommandCount; ++nIndex) {
if(supports(eNavigationCommands[nIndex])) {
pToolBar->addAction(getAction(eNavigationCommands[nIndex]));
bHasNavigation = true;
@@ -144,10 +159,13 @@ void nmVTKViewCommandController::populateContextMenu(QMenu* pMenu) const
ViewCommand_Front,
ViewCommand_Right,
ViewCommand_Isometric,
- ViewCommand_ResetRotationCenter
+ ViewCommand_ResetRotationCenter,
+ ViewCommand_SetRotationCenter
};
bool bHasViewCommands = false;
- for(int nIndex = 0; nIndex < 8; ++nIndex) {
+ const int nViewCommandCount =
+ sizeof(eViewCommands) / sizeof(eViewCommands[0]);
+ for(int nIndex = 0; nIndex < nViewCommandCount; ++nIndex) {
bHasViewCommands = bHasViewCommands || supports(eViewCommands[nIndex]);
}
if(bHasViewCommands) {
@@ -155,7 +173,7 @@ void nmVTKViewCommandController::populateContextMenu(QMenu* pMenu) const
pMenu->addSeparator();
}
QMenu* pViewMenu = pMenu->addMenu(tr("View"));
- for(int nIndex = 0; nIndex < 8; ++nIndex) {
+ for(int nIndex = 0; nIndex < nViewCommandCount; ++nIndex) {
if(supports(eViewCommands[nIndex])) {
pViewMenu->addAction(getAction(eViewCommands[nIndex]));
}
@@ -196,6 +214,25 @@ bool nmVTKViewCommandController::isViewAvailable() const
return m_bViewAvailable;
}
+void nmVTKViewCommandController::setRotationCenterPickingAvailable(
+ bool bAvailable)
+{
+ m_bRotationCenterPickingAvailable = bAvailable;
+ updateActionStates();
+}
+
+void nmVTKViewCommandController::setRotationCenterPicking(bool bPicking)
+{
+ QAction* pAction = getAction(ViewCommand_SetRotationCenter);
+ if(pAction == nullptr || !pAction->isCheckable()) {
+ return;
+ }
+
+ const bool bOldBlockState = pAction->blockSignals(true);
+ pAction->setChecked(bPicking);
+ pAction->blockSignals(bOldBlockState);
+}
+
QAction* nmVTKViewCommandController::createAction(
ViewCommand eCommand,
const QString& sText,
@@ -234,6 +271,7 @@ int nmVTKViewCommandController::actionIndex(ViewCommand eCommand) const
case ViewCommand_CopyImage: return 9;
case ViewCommand_Print: return 10;
case ViewCommand_PrintPreview: return 11;
+ case ViewCommand_SetRotationCenter: return 12;
default: return -1;
}
}
@@ -257,14 +295,23 @@ void nmVTKViewCommandController::updateActionStates()
ViewCommand_SaveImage,
ViewCommand_CopyImage,
ViewCommand_Print,
- ViewCommand_PrintPreview
+ ViewCommand_PrintPreview,
+ ViewCommand_SetRotationCenter
};
for(int nIndex = 0; nIndex < ViewCommandCount; ++nIndex) {
QAction* pAction = m_pActions[nIndex];
if(pAction != nullptr) {
const bool bSupported = supports(eCommands[nIndex]);
+ bool bEnabled = bSupported && m_bViewAvailable;
+ if(eCommands[nIndex] == ViewCommand_SetRotationCenter) {
+ bEnabled = bEnabled &&
+ m_bRotationCenterPickingAvailable;
+ if(!bEnabled) {
+ setRotationCenterPicking(false);
+ }
+ }
pAction->setVisible(bSupported);
- pAction->setEnabled(bSupported && m_bViewAvailable);
+ pAction->setEnabled(bEnabled);
}
}
}
diff --git a/Src/nmNum/nmRender/nmWxVTKRenderContainerWidget.cpp b/Src/nmNum/nmRender/nmWxVTKRenderContainerWidget.cpp
index b7b9750..460c9d7 100644
--- a/Src/nmNum/nmRender/nmWxVTKRenderContainerWidget.cpp
+++ b/Src/nmNum/nmRender/nmWxVTKRenderContainerWidget.cpp
@@ -4,6 +4,8 @@
#include "nmVTKImageExporter.h"
#include "nmVTKInteractionManager.h"
#include "nmVTKOrientationMarkerController.h"
+#include "nmVTKRotationCenterMarkerLayer.h"
+#include "nmVTKRotationCenterPicker.h"
#include "nmVTKScene.h"
#include "nmVTKViewCommandController.h"
@@ -16,6 +18,7 @@
#include
#include
#include
+#include
#include
#include
@@ -28,6 +31,13 @@ VTK_MODULE_INIT(vtkInteractionStyle)
#include
#include
+namespace {
+
+const char* const ROTATION_CENTER_MARKER_LAYER_ID =
+ "nm.render.rotation_center_marker";
+
+}
+
nmWxVTKRenderContainerWidget::nmWxVTKRenderContainerWidget(QWidget* pParent)
: QWidget(pParent),
m_pMainLayout(nullptr),
@@ -37,11 +47,14 @@ nmWxVTKRenderContainerWidget::nmWxVTKRenderContainerWidget(QWidget* pParent)
m_pVtkWidget(nullptr),
m_pRenderer(nullptr),
m_pScene(nullptr),
+ m_pSystemScene(nullptr),
m_pCameraController(nullptr),
m_pInteractionManager(nullptr),
m_pImageExporter(nullptr),
m_pViewCommandController(nullptr),
- m_pOrientationMarkerController(nullptr)
+ m_pOrientationMarkerController(nullptr),
+ m_pRotationCenterPicker(nullptr),
+ m_pRotationCenterMarkerLayer(nullptr)
{
Q_ASSERT(isGuiThread());
@@ -79,9 +92,26 @@ nmWxVTKRenderContainerWidget::nmWxVTKRenderContainerWidget(QWidget* pParent)
}
// 5. 管理对象、图片服务和命令对象均只服务当前容器实例。
+ // 业务图层和容器内部装饰分别管理;公开业务清场不会删除系统 Marker。
m_pScene = new nmVTKScene(m_pRenderer);
+ m_pSystemScene = new nmVTKScene(m_pRenderer);
m_pCameraController = new nmVTKCameraController(m_pRenderer);
- m_pInteractionManager = new nmVTKInteractionManager();
+ m_pInteractionManager = new nmVTKInteractionManager(
+ m_pCameraController);
+ nmVTKRotationCenterMarkerLayer* pRotationCenterMarkerLayer =
+ new nmVTKRotationCenterMarkerLayer(
+ ROTATION_CENTER_MARKER_LAYER_ID);
+ if(m_pSystemScene != nullptr &&
+ m_pSystemScene->addLayer(pRotationCenterMarkerLayer)) {
+ if(pRotationCenterMarkerLayer->bindRenderer(m_pRenderer)) {
+ m_pRotationCenterMarkerLayer = pRotationCenterMarkerLayer;
+ } else {
+ m_pSystemScene->removeLayer(ROTATION_CENTER_MARKER_LAYER_ID);
+ }
+ } else {
+ delete pRotationCenterMarkerLayer;
+ }
+ m_pRotationCenterPicker = new nmVTKRotationCenterPicker();
m_pImageExporter = new nmVTKImageExporter();
m_pImageExporter->setRenderWindow(pRenderWindow);
m_pViewCommandController =
@@ -103,8 +133,13 @@ nmWxVTKRenderContainerWidget::nmWxVTKRenderContainerWidget(QWidget* pParent)
connect(m_pViewCommandController,
SIGNAL(contextMenuAboutToShow(QMenu*)),
this, SIGNAL(contextMenuAboutToShow(QMenu*)));
+ connect(this, SIGNAL(rotationCenterPickingChanged(bool)),
+ m_pViewCommandController,
+ SLOT(setRotationCenterPicking(bool)));
const bool bInteractionReady =
- m_pInteractionManager->bindInteractor(pInteractor);
+ m_pInteractionManager->bindInteractor(
+ pInteractor, m_pRenderer);
+ updateRotationCenterCommandAvailability();
// 方向轴是独立辅助 Renderer;绑定失败只隐藏方向轴,不影响主视图初始化。
m_pOrientationMarkerController =
@@ -138,42 +173,60 @@ nmWxVTKRenderContainerWidget::~nmWxVTKRenderContainerWidget()
m_pViewCommandController = nullptr;
}
- // 3. MarkerWidget 持有 Interactor 和辅助 Renderer 引用,必须优先解绑。
+ // 3. 拾取器先恢复光标;场景标记必须在 Camera 和 Renderer 前解除观察关系。
+ if(m_pRotationCenterPicker != nullptr) {
+ m_pRotationCenterPicker->reset();
+ delete m_pRotationCenterPicker;
+ m_pRotationCenterPicker = nullptr;
+ }
+ nmVTKRotationCenterMarkerLayer* pRotationCenterMarkerLayer =
+ getRotationCenterMarkerLayer();
+ if(pRotationCenterMarkerLayer != nullptr) {
+ pRotationCenterMarkerLayer->unbindRenderer();
+ }
+
+ // 4. MarkerWidget 持有 Interactor 和辅助 Renderer 引用,必须优先解绑。
if(m_pOrientationMarkerController != nullptr) {
m_pOrientationMarkerController->unbindInteractor();
delete m_pOrientationMarkerController;
m_pOrientationMarkerController = nullptr;
}
- // 4. 图片服务不拥有 RenderWindow,先解除引用再销毁服务。
+ // 5. 图片服务不拥有 RenderWindow,先解除引用再销毁服务。
if(m_pImageExporter != nullptr) {
m_pImageExporter->setRenderWindow(nullptr);
delete m_pImageExporter;
m_pImageExporter = nullptr;
}
- // 5. Interactor 必须先释放对默认 Style 的 VTK 引用。
+ // 6. Interactor 必须先释放对默认 Style 的 VTK 引用。
if(m_pInteractionManager != nullptr) {
m_pInteractionManager->unbindInteractor();
delete m_pInteractionManager;
m_pInteractionManager = nullptr;
}
- // 6. Scene 在 Renderer 移除前清空所有 Layer 和 Prop。
+ // 7. 两个 Scene 均在 Camera 和 Renderer 前清空;系统装饰先解除所有权。
+ if(m_pSystemScene != nullptr) {
+ m_pRotationCenterMarkerLayer = nullptr;
+ m_pSystemScene->clear();
+ delete m_pSystemScene;
+ m_pSystemScene = nullptr;
+ }
if(m_pScene != nullptr) {
m_pScene->clear();
delete m_pScene;
m_pScene = nullptr;
}
- // 7. 相机控制器仅解除 Renderer 非所有权引用。
+ // 8. 相机控制器仅解除 Renderer 非所有权引用。
if(m_pCameraController != nullptr) {
m_pCameraController->setRenderer(nullptr);
delete m_pCameraController;
m_pCameraController = nullptr;
}
- // 8. 最后从 RenderWindow 移除并释放当前实例 Renderer。
+ // 9. 最后从 RenderWindow 移除并释放当前实例 Renderer。
vtkRenderWindow* pRenderWindow = getRenderWindow();
if(pRenderWindow != nullptr && m_pRenderer != nullptr) {
pRenderWindow->RemoveRenderer(m_pRenderer);
@@ -224,32 +277,84 @@ nmWxVTKRenderContainerWidget::getOrientationMarkerController() const
void nmWxVTKRenderContainerWidget::setViewCommandCapabilities(
nmVTKViewCommandController::ViewCommands eCommands)
{
+ if(!eCommands.testFlag(
+ nmVTKViewCommandController::ViewCommand_SetRotationCenter)) {
+ // 能力移除前先结束模式,QAction 仅在随后同步隐藏和禁用。
+ cancelRotationCenterPicking();
+ }
if(m_pViewCommandController != nullptr) {
m_pViewCommandController->setCapabilities(eCommands);
}
+ updateRotationCenterMarkerVisibility();
+}
+
+void nmWxVTKRenderContainerWidget::setRotationCenterPickTarget(
+ vtkProp* pTarget)
+{
+ Q_ASSERT(isGuiThread());
+ if(!isGuiThread() || m_pRotationCenterPicker == nullptr) {
+ return;
+ }
+
+ if(pTarget == nullptr ||
+ m_pRotationCenterPicker->getPickTarget() != pTarget) {
+ cancelRotationCenterPicking();
+ m_pRotationCenterPicker->setPickTarget(pTarget);
+ }
+ if(pTarget == nullptr) {
+ hideRotationCenterMarker();
+ }
+ updateRotationCenterCommandAvailability();
+ updateRotationCenterMarkerVisibility();
+}
+
+bool nmWxVTKRenderContainerWidget::isRotationCenterPicking() const
+{
+ return m_pRotationCenterPicker != nullptr &&
+ m_pRotationCenterPicker->isActive();
+}
+
+void nmWxVTKRenderContainerWidget::hideRotationCenterMarker()
+{
+ Q_ASSERT(isGuiThread());
+ nmVTKRotationCenterMarkerLayer* pMarkerLayer =
+ getRotationCenterMarkerLayer();
+ if(isGuiThread() && pMarkerLayer != nullptr) {
+ pMarkerLayer->setMarkerVisible(false);
+ }
}
bool nmWxVTKRenderContainerWidget::installInteractionStyle(
vtkInteractorStyle* pStyle)
{
Q_ASSERT(isGuiThread());
- return isGuiThread() && m_pInteractionManager != nullptr &&
+ cancelRotationCenterPicking();
+ const bool bSucceeded = isGuiThread() &&
+ m_pInteractionManager != nullptr &&
m_pInteractionManager->installInteractionStyle(pStyle);
+ updateRotationCenterCommandAvailability();
+ return bSucceeded;
}
bool nmWxVTKRenderContainerWidget::restoreDefaultInteraction()
{
Q_ASSERT(isGuiThread());
- return isGuiThread() && m_pInteractionManager != nullptr &&
+ cancelRotationCenterPicking();
+ const bool bSucceeded = isGuiThread() &&
+ m_pInteractionManager != nullptr &&
m_pInteractionManager->restoreDefaultInteraction();
+ updateRotationCenterCommandAvailability();
+ return bSucceeded;
}
void nmWxVTKRenderContainerWidget::clearInteractionStyle()
{
Q_ASSERT(isGuiThread());
+ cancelRotationCenterPicking();
if(isGuiThread() && m_pInteractionManager != nullptr) {
m_pInteractionManager->clearInteractionStyle();
}
+ updateRotationCenterCommandAvailability();
}
void nmWxVTKRenderContainerWidget::renderView()
@@ -258,6 +363,7 @@ void nmWxVTKRenderContainerWidget::renderView()
vtkRenderWindow* pRenderWindow = getRenderWindow();
// Scene、Layer 和相机均不隐式渲染,批量更新最终汇聚到此入口。
if(isGuiThread() && pRenderWindow != nullptr) {
+ synchronizeRotationCenterMarker();
pRenderWindow->Render();
}
}
@@ -265,11 +371,29 @@ void nmWxVTKRenderContainerWidget::renderView()
bool nmWxVTKRenderContainerWidget::fitTopViewWithoutRender()
{
Q_ASSERT(isGuiThread());
+ cancelRotationCenterPicking();
// 相机控制器本身不触发 Render,便于业务图层完成整批更新后只渲染一次。
return isGuiThread() && m_pCameraController != nullptr &&
m_pCameraController->setTopView();
}
+bool nmWxVTKRenderContainerWidget::resetRotationCenterWithoutRender()
+{
+ Q_ASSERT(isGuiThread());
+ cancelRotationCenterPicking();
+ if(!isGuiThread() || m_pCameraController == nullptr ||
+ !m_pCameraController->resetRotationCenter()) {
+ return false;
+ }
+
+ double dCenter[3];
+ if(!m_pCameraController->getRotationCenter(dCenter)) {
+ return false;
+ }
+ return applyRotationCenter(
+ dCenter[0], dCenter[1], dCenter[2], true);
+}
+
void nmWxVTKRenderContainerWidget::showReadyState()
{
Q_ASSERT(isGuiThread());
@@ -281,6 +405,7 @@ void nmWxVTKRenderContainerWidget::showReadyState()
if(m_pViewCommandController != nullptr) {
m_pViewCommandController->setViewAvailable(true);
}
+ updateRotationCenterMarkerVisibility();
if(m_pOrientationMarkerController != nullptr) {
m_pOrientationMarkerController->setViewAvailable(true);
}
@@ -305,6 +430,7 @@ void nmWxVTKRenderContainerWidget::showRenderingState(
void nmWxVTKRenderContainerWidget::zoomIn()
{
+ cancelRotationCenterPicking();
if(m_pCameraController != nullptr && m_pCameraController->zoomIn()) {
renderView();
}
@@ -312,6 +438,7 @@ void nmWxVTKRenderContainerWidget::zoomIn()
void nmWxVTKRenderContainerWidget::zoomOut()
{
+ cancelRotationCenterPicking();
if(m_pCameraController != nullptr && m_pCameraController->zoomOut()) {
renderView();
}
@@ -319,6 +446,7 @@ void nmWxVTKRenderContainerWidget::zoomOut()
void nmWxVTKRenderContainerWidget::fitView()
{
+ cancelRotationCenterPicking();
if(m_pCameraController != nullptr && m_pCameraController->fitView()) {
renderView();
}
@@ -326,6 +454,7 @@ void nmWxVTKRenderContainerWidget::fitView()
void nmWxVTKRenderContainerWidget::resetCamera()
{
+ cancelRotationCenterPicking();
// 控制器用返回值表示相机是否确实改变,成功时只提交一次渲染。
if(m_pCameraController != nullptr && m_pCameraController->resetCamera()) {
renderView();
@@ -334,6 +463,7 @@ void nmWxVTKRenderContainerWidget::resetCamera()
void nmWxVTKRenderContainerWidget::setTopView()
{
+ cancelRotationCenterPicking();
if(m_pCameraController != nullptr && m_pCameraController->setTopView()) {
renderView();
}
@@ -341,6 +471,7 @@ void nmWxVTKRenderContainerWidget::setTopView()
void nmWxVTKRenderContainerWidget::setFrontView()
{
+ cancelRotationCenterPicking();
if(m_pCameraController != nullptr && m_pCameraController->setFrontView()) {
renderView();
}
@@ -348,6 +479,7 @@ void nmWxVTKRenderContainerWidget::setFrontView()
void nmWxVTKRenderContainerWidget::setRightView()
{
+ cancelRotationCenterPicking();
if(m_pCameraController != nullptr && m_pCameraController->setRightView()) {
renderView();
}
@@ -355,6 +487,7 @@ void nmWxVTKRenderContainerWidget::setRightView()
void nmWxVTKRenderContainerWidget::setIsometricView()
{
+ cancelRotationCenterPicking();
if(m_pCameraController != nullptr &&
m_pCameraController->setIsometricView()) {
renderView();
@@ -366,26 +499,201 @@ void nmWxVTKRenderContainerWidget::setRotationCenter(
double dY,
double dZ)
{
- if(m_pCameraController != nullptr &&
- m_pCameraController->setRotationCenter(dX, dY, dZ)) {
+ cancelRotationCenterPicking();
+ if(applyRotationCenter(dX, dY, dZ, true)) {
renderView();
}
}
void nmWxVTKRenderContainerWidget::resetRotationCenter()
{
- if(m_pCameraController != nullptr &&
- m_pCameraController->resetRotationCenter()) {
+ if(resetRotationCenterWithoutRender()) {
renderView();
}
}
+bool nmWxVTKRenderContainerWidget::applyRotationCenter(
+ double dX,
+ double dY,
+ double dZ,
+ bool bShowMarker)
+{
+ Q_ASSERT(isGuiThread());
+ if(!isGuiThread() || m_pCameraController == nullptr ||
+ !m_pCameraController->setRotationCenter(dX, dY, dZ)) {
+ return false;
+ }
+
+ // CameraController 是独立枢轴的唯一逻辑状态源;Marker 只保存显示副本。
+ nmVTKRotationCenterMarkerLayer* pMarkerLayer =
+ getRotationCenterMarkerLayer();
+ if(pMarkerLayer != nullptr) {
+ if(!pMarkerLayer->setWorldCenter(dX, dY, dZ)) {
+ return false;
+ }
+ if(bShowMarker) {
+ pMarkerLayer->setMarkerVisible(true);
+ }
+ }
+ synchronizeRotationCenterMarker();
+ return true;
+}
+
+void nmWxVTKRenderContainerWidget::cancelRotationCenterPicking()
+{
+ Q_ASSERT(isGuiThread());
+ if(!isGuiThread()) {
+ return;
+ }
+
+ if(m_pRotationCenterPicker != nullptr) {
+ // reset 同时清除待消费 Release,并按进入模式前状态恢复光标。
+ m_pRotationCenterPicker->reset();
+ }
+ emit rotationCenterPickingChanged(false);
+}
+
+bool nmWxVTKRenderContainerWidget::handleRotationCenterPickEvent(
+ QEvent* pEvent)
+{
+ if(m_pRotationCenterPicker == nullptr || pEvent == nullptr) {
+ return false;
+ }
+
+ const nmVTKRotationCenterPicker::PickEventResult oResult =
+ m_pRotationCenterPicker->handleEvent(pEvent);
+ if(oResult.eOutcome ==
+ nmVTKRotationCenterPicker::PickOutcome_Hit) {
+ double dPosition[3];
+ if(m_pRotationCenterPicker->getPickedPosition(dPosition) &&
+ applyRotationCenter(dPosition[0], dPosition[1],
+ dPosition[2], true)) {
+ // 独立旋转中心和 Marker 已作为一批提交,只渲染一次。
+ renderView();
+ }
+ }
+
+ if(oResult.eOutcome !=
+ nmVTKRotationCenterPicker::PickOutcome_None) {
+ emit rotationCenterPickingChanged(false);
+ }
+ return oResult.bConsumed;
+}
+
+void nmWxVTKRenderContainerWidget::synchronizeRotationCenterMarker()
+{
+ nmVTKRotationCenterMarkerLayer* pMarkerLayer =
+ getRotationCenterMarkerLayer();
+ if(pMarkerLayer != nullptr && pMarkerLayer->isMarkerVisible()) {
+ // synchronize 只更新世界几何,绝不从图层内部触发 Render。
+ pMarkerLayer->synchronize();
+ }
+}
+
+void nmWxVTKRenderContainerWidget::updateRotationCenterMarkerVisibility()
+{
+ nmVTKRotationCenterMarkerLayer* pMarkerLayer =
+ getRotationCenterMarkerLayer();
+ if(pMarkerLayer == nullptr) {
+ return;
+ }
+
+ const bool bVisible = m_pViewCommandController != nullptr &&
+ m_pViewCommandController->isViewAvailable() &&
+ m_pViewCommandController->getCapabilities().testFlag(
+ nmVTKViewCommandController::ViewCommand_SetRotationCenter) &&
+ m_pRotationCenterPicker != nullptr &&
+ m_pRotationCenterPicker->hasPickTarget() &&
+ pMarkerLayer->hasWorldCenter();
+ pMarkerLayer->setMarkerVisible(bVisible);
+}
+
+void nmWxVTKRenderContainerWidget::updateRotationCenterCommandAvailability()
+{
+ if(m_pViewCommandController == nullptr) {
+ return;
+ }
+
+ const bool bAvailable = m_pRotationCenterPicker != nullptr &&
+ m_pRotationCenterPicker->hasPickTarget() &&
+ getRotationCenterMarkerLayer() != nullptr &&
+ m_pInteractionManager != nullptr &&
+ m_pInteractionManager->isDefaultInteractionActive();
+ m_pViewCommandController->setRotationCenterPickingAvailable(
+ bAvailable);
+}
+
+nmVTKRotationCenterMarkerLayer*
+nmWxVTKRenderContainerWidget::getRotationCenterMarkerLayer() const
+{
+ if(m_pSystemScene == nullptr || m_pRotationCenterMarkerLayer == nullptr ||
+ m_pSystemScene->getLayer(ROTATION_CENTER_MARKER_LAYER_ID) !=
+ m_pRotationCenterMarkerLayer) {
+ return nullptr;
+ }
+ return m_pRotationCenterMarkerLayer;
+}
+
+void nmWxVTKRenderContainerWidget::setRotationCenterPickingEnabled(
+ bool bEnabled)
+{
+ Q_ASSERT(isGuiThread());
+ if(!isGuiThread()) {
+ return;
+ }
+
+ if(!bEnabled) {
+ cancelRotationCenterPicking();
+ return;
+ }
+
+ const bool bCommandAvailable = m_pViewCommandController != nullptr &&
+ m_pViewCommandController->isViewAvailable() &&
+ m_pViewCommandController->getCapabilities().testFlag(
+ nmVTKViewCommandController::ViewCommand_SetRotationCenter);
+ const bool bCanStart = bCommandAvailable &&
+ m_pRotationCenterPicker != nullptr &&
+ m_pRotationCenterPicker->getPickTarget() != nullptr &&
+ getRotationCenterMarkerLayer() != nullptr &&
+ m_pInteractionManager != nullptr &&
+ m_pInteractionManager->isDefaultInteractionActive();
+ if(!bCanStart ||
+ !m_pRotationCenterPicker->start(m_pVtkWidget, m_pRenderer)) {
+ if(m_pRotationCenterPicker != nullptr) {
+ m_pRotationCenterPicker->reset();
+ }
+ updateRotationCenterCommandAvailability();
+ emit rotationCenterPickingChanged(false);
+ return;
+ }
+
+ emit rotationCenterPickingChanged(true);
+}
+
bool nmWxVTKRenderContainerWidget::eventFilter(
QObject* pObject,
QEvent* pEvent)
{
- if(pObject == m_pVtkWidget && pEvent != nullptr &&
- pEvent->type() == QEvent::MouseButtonPress) {
+ if(pObject != m_pVtkWidget || pEvent == nullptr) {
+ return QWidget::eventFilter(pObject, pEvent);
+ }
+
+ // 一次性拾取优先于 VTK 相机交互和右键菜单,并继续吞掉对应 Release。
+ if(m_pRotationCenterPicker != nullptr &&
+ (m_pRotationCenterPicker->isActive() ||
+ m_pRotationCenterPicker->hasPendingRelease()) &&
+ handleRotationCenterPickEvent(pEvent)) {
+ return true;
+ }
+
+ if(pEvent->type() == QEvent::FocusOut) {
+ // 延后到本轮焦点切换结束,避免点击工具按钮启动模式时被旧 FocusOut 取消。
+ QTimer::singleShot(
+ 0, this,
+ SLOT(cancelRotationCenterPickingAfterFocusOut()));
+ }
+
+ if(pEvent->type() == QEvent::MouseButtonPress) {
QMouseEvent* pMouseEvent = static_cast(pEvent);
if(pMouseEvent->button() == Qt::RightButton) {
// 菜单引用命令控制器长期持有的 QAction,不在每次右键时重复创建。
@@ -404,11 +712,22 @@ bool nmWxVTKRenderContainerWidget::eventFilter(
return QWidget::eventFilter(pObject, pEvent);
}
+void nmWxVTKRenderContainerWidget::cancelRotationCenterPickingAfterFocusOut()
+{
+ if(m_pVtkWidget != nullptr && !m_pVtkWidget->hasFocus() &&
+ m_pRotationCenterPicker != nullptr &&
+ (m_pRotationCenterPicker->isActive() ||
+ m_pRotationCenterPicker->hasPendingRelease())) {
+ cancelRotationCenterPicking();
+ }
+}
+
void nmWxVTKRenderContainerWidget::saveImage()
{
if(m_pImageExporter == nullptr) {
return;
}
+ synchronizeRotationCenterMarker();
// 实际编码格式由 QImage 按调用方选择的扩展名决定。
const QString sFileName = QFileDialog::getSaveFileName(
this,
@@ -424,6 +743,7 @@ void nmWxVTKRenderContainerWidget::saveImage()
void nmWxVTKRenderContainerWidget::copyImage()
{
+ synchronizeRotationCenterMarker();
if(m_pImageExporter != nullptr &&
!m_pImageExporter->copyImageToClipboard()) {
QMessageBox::warning(this,
@@ -435,6 +755,7 @@ void nmWxVTKRenderContainerWidget::copyImage()
void nmWxVTKRenderContainerWidget::printImage()
{
if(m_pImageExporter != nullptr) {
+ synchronizeRotationCenterMarker();
m_pImageExporter->printImage(this);
}
}
@@ -442,6 +763,7 @@ void nmWxVTKRenderContainerWidget::printImage()
void nmWxVTKRenderContainerWidget::printPreview()
{
if(m_pImageExporter != nullptr) {
+ synchronizeRotationCenterMarker();
m_pImageExporter->printPreview(this);
}
}
@@ -464,6 +786,9 @@ void nmWxVTKRenderContainerWidget::showMessageState(
}
m_pStateLabel->setText(sMessage);
m_pStateStack->setCurrentWidget(m_pStatePage);
+ // 非就绪状态先结束临时工具并清除手工标记,再禁用和取消 QAction。
+ cancelRotationCenterPicking();
+ hideRotationCenterMarker();
if(m_pViewCommandController != nullptr) {
m_pViewCommandController->setViewAvailable(false);
}
diff --git a/Src/nmNum/nmSubWxs/nmPebiGridSceneController.cpp b/Src/nmNum/nmSubWxs/nmPebiGridSceneController.cpp
index daf4cf6..0f6eaab 100644
--- a/Src/nmNum/nmSubWxs/nmPebiGridSceneController.cpp
+++ b/Src/nmNum/nmSubWxs/nmPebiGridSceneController.cpp
@@ -202,11 +202,20 @@ public:
// 属性不可用只回退固定面色,不影响已经验证成功的网格几何提交。
applyProperty(sRequestedProperty);
+ // 只有整份网格绑定成功后才允许公共拾取器命中主 Mesh Actor。
+ if(m_pContainer != nullptr) {
+ m_pContainer->setRotationCenterPickTarget(
+ m_pMeshLayer->getActor());
+ }
return true;
}
void releaseGridData()
{
+ // 先取消临时拾取并清除弱目标,再解除 Mapper 对网格的共享引用。
+ if(m_pContainer != nullptr) {
+ m_pContainer->setRotationCenterPickTarget(nullptr);
+ }
// 先断开 Mapper 对旧网格的引用,再释放 ViewData 的共享引用。
if(m_pMeshLayer != nullptr) {
m_pMeshLayer->setScalarVisibility(false);
@@ -321,8 +330,10 @@ bool nmPebiGridSceneController::setViewData(
return false;
}
- // 几何变化始终恢复正交顶视图并适配;整批更新最终只在就绪入口渲染一次。
+ // 几何变化恢复正交顶视图,并将独立枢轴初始化为新网格可见边界中心。
+ // 两步都不立即渲染,整批更新最终只在就绪入口提交一次。
m_pPrivate->m_pContainer->fitTopViewWithoutRender();
+ m_pPrivate->m_pContainer->resetRotationCenterWithoutRender();
m_pPrivate->m_pContainer->showReadyState();
return true;
}
diff --git a/Src/nmNum/nmSubWxs/nmWxPebiGridViewWidget.cpp b/Src/nmNum/nmSubWxs/nmWxPebiGridViewWidget.cpp
index 35792e0..16235d9 100644
--- a/Src/nmNum/nmSubWxs/nmWxPebiGridViewWidget.cpp
+++ b/Src/nmNum/nmSubWxs/nmWxPebiGridViewWidget.cpp
@@ -78,6 +78,7 @@ void nmWxPebiGridViewWidget::createUi()
nmVTKViewCommandController::ViewCommand_ZoomOut |
nmVTKViewCommandController::ViewCommand_Fit |
nmVTKViewCommandController::ViewCommand_Top |
+ nmVTKViewCommandController::ViewCommand_SetRotationCenter |
nmVTKViewCommandController::ViewCommand_SaveImage |
nmVTKViewCommandController::ViewCommand_CopyImage |
nmVTKViewCommandController::ViewCommand_Print |
@@ -87,7 +88,7 @@ void nmWxPebiGridViewWidget::createUi()
m_pToolBar->setOrientation(Qt::Vertical);
m_pToolBar->setMovable(false);
m_pToolBar->setFloatable(false);
- m_pToolBar->setIconSize(QSize(24, 24));
+ m_pToolBar->setIconSize(QSize(32, 32));
m_pToolBar->setToolButtonStyle(Qt::ToolButtonIconOnly);
m_pToolBar->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);
@@ -121,6 +122,9 @@ void nmWxPebiGridViewWidget::createUi()
nmVTKViewCommandController::ViewCommand_ZoomIn);
QAction* pTopAction = pCommandController->getAction(
nmVTKViewCommandController::ViewCommand_Top);
+ QAction* pSetRotationCenterAction = pCommandController->getAction(
+ nmVTKViewCommandController::
+ ViewCommand_SetRotationCenter);
QAction* pCopyAction = pCommandController->getAction(
nmVTKViewCommandController::ViewCommand_CopyImage);
bool bHasPreviousGroup = false;
@@ -136,13 +140,19 @@ void nmWxPebiGridViewWidget::createUi()
m_pToolBar->addAction(pZoomInAction);
bHasPreviousGroup = true;
}
+ const bool bHasViewGroup = pTopAction != nullptr ||
+ pSetRotationCenterAction != nullptr;
+ if(bHasViewGroup && bHasPreviousGroup) {
+ m_pToolBar->addSeparator();
+ }
if(pTopAction != nullptr) {
- if(bHasPreviousGroup) {
- m_pToolBar->addSeparator();
- }
m_pToolBar->addAction(pTopAction);
bHasPreviousGroup = true;
}
+ if(pSetRotationCenterAction != nullptr) {
+ m_pToolBar->addAction(pSetRotationCenterAction);
+ bHasPreviousGroup = true;
+ }
if(bHasPreviousGroup) {
m_pToolBar->addSeparator();
}
@@ -260,6 +270,7 @@ void nmWxPebiGridViewWidget::showFailure(const QString& sMessage)
{
if(m_bHasViewData) {
// 失败时旧网格引用保持不变,状态条明确说明当前画面并非本次成果。
+ m_pRenderContainer->setRotationCenterPickingEnabled(false);
m_pStatusLabel->setText(sMessage);
m_pStatusLabel->show();
setControlsEnabled(true);
diff --git a/Src4/nmNum/nmRender/nmRender.pro b/Src4/nmNum/nmRender/nmRender.pro
index f2f7ebf..3b53f9e 100644
--- a/Src4/nmNum/nmRender/nmRender.pro
+++ b/Src4/nmNum/nmRender/nmRender.pro
@@ -20,9 +20,12 @@ SOURCES += $${wtSrc}/nmNum/nmRender/nmVTKCameraController.cpp \
$${wtSrc}/nmNum/nmRender/nmVTKImageExporter.cpp \
$${wtSrc}/nmNum/nmRender/nmVTKInteractionManager.cpp \
$${wtSrc}/nmNum/nmRender/nmVTKOrientationMarkerController.cpp \
+ $${wtSrc}/nmNum/nmRender/nmVTKPivotCameraInteractorStyle.cpp \
$${wtSrc}/nmNum/nmRender/nmVTKMeshLayer.cpp \
$${wtSrc}/nmNum/nmRender/nmVTKPlanarScaleGridLayer.cpp \
$${wtSrc}/nmNum/nmRender/nmVTKRenderLayer.cpp \
+ $${wtSrc}/nmNum/nmRender/nmVTKRotationCenterMarkerLayer.cpp \
+ $${wtSrc}/nmNum/nmRender/nmVTKRotationCenterPicker.cpp \
$${wtSrc}/nmNum/nmRender/nmVTKScalarBarLayer.cpp \
$${wtSrc}/nmNum/nmRender/nmVTKScene.cpp \
$${wtSrc}/nmNum/nmRender/nmVTKViewCommandController.cpp \
@@ -34,9 +37,12 @@ HEADERS += $${wtInclude}/nmNum/nmRender/nmRender_global.h \
$${wtInclude}/nmNum/nmRender/nmVTKImageExporter.h \
$${wtInclude}/nmNum/nmRender/nmVTKInteractionManager.h \
$${wtInclude}/nmNum/nmRender/nmVTKOrientationMarkerController.h \
+ $${wtInclude}/nmNum/nmRender/nmVTKPivotCameraInteractorStyle.h \
$${wtInclude}/nmNum/nmRender/nmVTKMeshLayer.h \
$${wtInclude}/nmNum/nmRender/nmVTKPlanarScaleGridLayer.h \
$${wtInclude}/nmNum/nmRender/nmVTKRenderLayer.h \
+ $${wtInclude}/nmNum/nmRender/nmVTKRotationCenterMarkerLayer.h \
+ $${wtInclude}/nmNum/nmRender/nmVTKRotationCenterPicker.h \
$${wtInclude}/nmNum/nmRender/nmVTKScalarBarLayer.h \
$${wtInclude}/nmNum/nmRender/nmVTKScene.h \
$${wtInclude}/nmNum/nmRender/nmVTKViewCommandController.h \