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 \