From 291aa998c6cef1491b9d7d528a81729fc1e58165 Mon Sep 17 00:00:00 2001 From: lh <2334563547@qq.com> Date: Tue, 15 Sep 2026 10:22:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(nmRender):=20=E5=AE=9E=E7=8E=B0=E9=80=9A?= =?UTF-8?q?=E7=94=A8=E6=97=8B=E8=BD=AC=E4=B8=AD=E5=BF=83=E6=8B=BE=E5=8F=96?= =?UTF-8?q?=E4=B8=8E=E7=8B=AC=E7=AB=8B=E6=9E=A2=E8=BD=B4=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增主网格一次性拾取和固定像素三轴中心标记 - 使用独立枢轴 Trackball,设置中心不再移动画面 - 完善 QAction、光标、交互模式及容器生命周期同步 - 隔离系统与业务 Scene,收口命令状态写接口 - 接入 PEBI 工具栏并同步工程、翻译和开发清单 --- Bin/Config/Lang/cn/nmNum_cn.qm | Bin 141196 -> 141282 bytes Bin/Config/Lang/cn/nmNum_cn.ts | 1 + .../nmNum/nmRender/nmVTKCameraController.h | 12 +- .../nmNum/nmRender/nmVTKInteractionManager.h | 18 +- .../nmVTKPivotCameraInteractorStyle.h | 54 ++ .../nmRender/nmVTKRotationCenterMarkerLayer.h | 56 ++ .../nmRender/nmVTKRotationCenterPicker.h | 96 ++++ .../nmRender/nmVTKViewCommandController.h | 23 +- .../nmRender/nmWxVTKRenderContainerWidget.h | 55 +- Src/nmNum/nmRender/nmVTKCameraController.cpp | 53 +- .../nmRender/nmVTKInteractionManager.cpp | 34 +- .../nmVTKPivotCameraInteractorStyle.cpp | 295 ++++++++++ .../nmVTKRotationCenterMarkerLayer.cpp | 530 ++++++++++++++++++ .../nmRender/nmVTKRotationCenterPicker.cpp | 316 +++++++++++ .../nmRender/nmVTKViewCommandController.cpp | 63 ++- .../nmRender/nmWxVTKRenderContainerWidget.cpp | 359 +++++++++++- .../nmSubWxs/nmPebiGridSceneController.cpp | 13 +- Src/nmNum/nmSubWxs/nmWxPebiGridViewWidget.cpp | 19 +- Src4/nmNum/nmRender/nmRender.pro | 6 + 19 files changed, 1929 insertions(+), 74 deletions(-) create mode 100644 Include/nmNum/nmRender/nmVTKPivotCameraInteractorStyle.h create mode 100644 Include/nmNum/nmRender/nmVTKRotationCenterMarkerLayer.h create mode 100644 Include/nmNum/nmRender/nmVTKRotationCenterPicker.h create mode 100644 Src/nmNum/nmRender/nmVTKPivotCameraInteractorStyle.cpp create mode 100644 Src/nmNum/nmRender/nmVTKRotationCenterMarkerLayer.cpp create mode 100644 Src/nmNum/nmRender/nmVTKRotationCenterPicker.cpp diff --git a/Bin/Config/Lang/cn/nmNum_cn.qm b/Bin/Config/Lang/cn/nmNum_cn.qm index f040dfd33c4f036a9094912abe2170b622af159d..2b422c0edebddca06bf992924d1166d9a4c76753 100644 GIT binary patch delta 8727 zcmXY$30#cZ8^@nB?>q0zJM+$rvW!q8OQW=^kfkJ1R46W$ig3x6om?7mlckU&%Dxmg z%1-uZL0v5*glv(0=eoG~KRSP(PoHn+HO-v!EWhV@&LmeTA66)v_BcMKdFh)?X=T=} z@<(nzQ`;NB4N{xhsOGc>fZ;%nYEpd-gQIW}iVJUK7{Kn|k>i0_ZbnW9;gMo|%l?^1)L;(H|Af-)Hk9vs*aIp@E^X$~`PNpZg$izi1ayP)v9RTv~0I6;b zByDAkodz-bBr^8h!r1>Td;qi>oD;ekGZt(rc#sJ3c`x#G5jCMu!?3Zf`86ON} ze0qfO#W=>-Z)r6#n2o{}{(MV8{to2i5-OJ&O>1yf3S`Sk06`B#5srKhL`3r`+<>U_ z09>>{{F(st!^3Vk%y^YDzC!c)+2ixajHl4Ne*1v*!B6=`zc-8n&M}5)0Rk5Tu~@)p zw+PKV*ab*me}G_ffPX$Cahsn9H;}Y&XKZ6nt7Qi7kpLk}fwURU=&}p>1J}zLU2Xyl zy#S=G3-UjJudNzLTCQSTluq3_8(D|eG=kH+;KMKs5!VgK=|G0#S;J}oa=jWzI6KA; zB>>?gfyC6&EUurJ0WcvCE%uOp;+$-E0?cR(9lsNe{+rEr7o{1IT!D;9oienRlMCFplwl z1<3gdAVaD^?miQUZ$7Q%y(|hr?&l6pLnkF~chJk!Lp38MC<>alDv&w1LD-3*lUvt7qMQT5#WWz93SDO)m2GLRU?35+ zOwjxP4T@jqIyDs(cn-2>BPa^-J@##BAknl2#jl;7cnV4fjM+<_Kyv{-Hsum%|2v3z zvK(~7Fdx?DQwN2O-DD?dm4O@2dd7HLg7)Lr1IgR-9)=`YXC!pJ3Jum{@7+0A?@61Rb z_Km>TZxM>qGUyv{9*FI3@SEcSB($7yYa#e8!{|H1G2X3*{v8)VN3z!d{X3(Hy5}>- z-(%b~k#X;J#-p>SuhPccUk?Ep(Lh3GLckL|wZAW2pzPbNs2>bT@B}d1#Ms9khAdtV zWZwoDwt5H-LJ){`n2Rli80UGJ^s+9|z-7Hvzf#8phAYL~Yy_#_uu$T(}7_=0{M@PC?Ar zRDgoW1`_2%#+xl@QI0)QVf-_jYSc#CD5zLe4P@F)s7$H!n-3@A%s!&#Z;EpN_!=j!> zsP(oP@ZvT~-nLr}Bz#+VxBd{2&Vlex1%`au5CXUU1oD>$-K_SMowuj8YNPlsX|?SE zklVpjBk9FlV#I<cYeNT2K}0P&?HNWTwAZD$hP8)NlZJPB9a0MK?I zQC4?>1gXf_k6SS3W69WmkRJlcIMW%dIzPzx-7!GsUMKNp!vGAo$fB;l-sdMRlk~Qg zWJQmo=$NS_c_%95@irtqY%CD^o@C5k36R)>Y#oFSY!yc}8od}xGQ9~9J2UE_(VK>o z+|l@&m|U`dL_9$CT5?#zY`fNl6j`9MuW=?N6Se?c?oX~H_X4u?8M*%9ERex#$b${Z z05#3XLp~Bn<{VNty9!I!cv4q@dpR+JJgY*dRTPug?_L9Gwv2r1X$!Dr9OL(Y$@jh4 z7}&SeGSUgt{$_-X?}H{X<;lcGVL+a~lu7w`#2aNYP1lb=h84=R<8fU#L#F!&O(JMy z&HPcUPAFt;hfM_%^;Fi8#$o0>l{s!k)1}UoxmRLY$m}5dB_E{i4eFpZSVYOX27ChI z(q7gz=o4-~Mdp>^2qbV49jomk=E#QBt^_jrH=2d79U+@`1&?&0P&WNmIlzkZvY8*P zG43bQTCIWP(C=D&8MPjT9>=WK#n+YW~m+k#~v}hJJCQg zWDqTD;W^z74TOiAvsodNt<`d^vDg&_o%K5#) zE^f$r&VMVqdW!|uKY0xP^0$y~@DT!*WUDg z+ni?Xq~eBOz(}4pgd1^YJCNNg=w@@H={Pt22)1pqa&CS!hF|(NZXJyTSo(uopNM;+ zp4|407^cTMb2|oR0jz${{Z+dYNJ=TUS3r?lwuw7fh%V~>4|n7$hV`fp-0_@hv|v&L ziRl)1=FJp<`%Ag}4`})ak=(g2qkt^%=gw!Ni%gX@-1$8e$Q4VjI0Aig-;BFhj157H zW89^j(*R%Uxzem105`sHmt*mD_giyS*KnilMsf`ad$J$*!t*td{MFp6s=H`Z1@|@( zKk&v{?$f|-SWY=c*K3U4+1!_)_gI#*cyi?gghSa)UU3EOcIX5z+M@3#Pvxbd0YHit zG2Z{mYZDFvWDI6}v5jw36N($C;?0(t1Iei6U8WtvR7vICkKo44C-B~J#Teo{sAUs_ zX*C}fSc8c&iH|5j*?v~bM_i4>er_utH5%>lYym&I8J3V|x%|vdw}33N;}b^Uw!2;6 z6MIHue|?ot$-B)(8;~8H_$@1ofJ~mjc*l<4dL|ygeigqnAIte4 z-T2JfZCE`@`K(@=_+qXM@6Vb3E%koNK}m|J^#dc3J}$fTl|afA93KYr^U@2doS%M=N1$K ztRW2~;eK*n+YZ1oj&a~sxqK6T&b0u>*Zbs(%Q)#dWXq*(vw#F&r0)74^FbbRt8dS6 zD5{nl`eR@YOrvRfo3I`}a;KSX03w6suCiPpUC+tg68m9;7AEgZkXk)sO@X{~Kp$*R z0_0u8=c5ixV7xj`?z{IZwvQ2Vzb4rz(f*8aO=#2RhQT-GBcl@lqQm8*n(YRnxye|Q zARqr7E#CK>Jf;`sR(vMoqrUQ(XI<*|J4lLJ65YvhOH%dpI6%Jb|tttf~>(m(BsA`^I?ZGtF(G_b3r;mtim_W(szJ9FXeGf@3SR%awG&<#!ajXU_z; z&W(Zi>=QaSn+L>uq|iGs2uRaF`n`qGv{4Al{S)A<72`iAk(n4QsY2ACYuF}c3sF;% zAC?PI-?5k!ItkH{X_&f0gy>=v#p#cQNteS=TH7!ld@fAu?1FZ)6{dN80J6MDm_H1Q zR$Cck*a%@+EEbg}dxYiD7`$|6OEdo*Va2AIK$a{KR@S@*SoK**J&BEB%Y0$Y4D`Y5 zNMTLVT^xgE2(-8zkP&H&tLufe4$m=tPcwR)rQcf`O?!oPStZ!1yk+dyNZ9xS*ZNS# zxi-S)k=AJaT1HPhVe1*RPySmWt8@^G#~Wdn9Sff_LZRA z|CS*fs2mCqyGJ;7DkN#>hmk4*quSCO4rE*JS=xE{X!LjHph4A*BBalH! z!iVtB*aqb&gx+XArwfd3`HZKUC=_4Y;JcM7T8zf5+nwG(^4w8jT&u-SyhP#Tw+E%; zAx*S2SRYro#iF4XuTpsQ#84>fsOUbgC#Fz8g_oNvkd?`_*wRR*&{vjD9`h6-Q*dAR zvJ^w&w*Zu;D~A7xW(jpsjH|#zc&k>#7;Uj@k5I&PjKaWZt(edW&15y44z=n+CejqE zE}`KHQzpL0>Nbjn&>vgY$&8!q6$#zY^tr`~r2UvM(mO@cfkM0`@Kq!|jR)xdOtE~v z7VEhgaTa%bF`vOt^>ER@BnqWqn<-H_-=3!9K6aBQ5QxOn{$kS^U7)m1@wA!eqy{>>As zcctP+R5YsSEymmH6*r$?kEhF3)Mzj%XLu>@53j~>P$}wa#{k*fgT69Y$>Qy)rnS*x zq_S!GV*F@7Wy>}(ctCq)tD_r$1k9y@t)0XIWrqxWdNzh8;;LThQG`Y~?M=_LHh2$H z`rg6(a+}9k;*LCuefBz~f8iK_Ln_8o0~ud_X&{;6r5s>DagU9pmezWUOUj{3Fa&Qp zD~H|x4?8Vi#{1i7ptXS*GrF4<{ZQA#)l{m^mE2;_q_6Oiz0xk*2-h?_&iCiJRaE;2P~QL zbhJB;R`$x%lO1vJs8XI4I24?K29oe6^h_Ho^H!iN&HjWJ|r*ke&siXPu*E;ikdA(%Huw6;L~Yt=<;(-rVYQ^a;3wLp6B z4Hes0-o@_bndr2<2aqQVMAy3|XoGsOOUF>0*_EPKp)Y{rNwIg)avPdS1syfg*!dk&M+!e+;^}P)8@W-@@xT$hFn%iD)wnJr(PvPo>29k>tXm$I(vJD1mX`?6ask@Dh%DRQhr!gMB#6-v1^p#ch zrnxp&ZNgksQ=VcyH#)0k)uSn^=BVa`p~*`6sN!}v0dlp7zOwN&#cEZ5=Hkc8!c{Am zp}U82s#P6spcfCQRxOvIw=b(!51#{MVGq?hPk-!A!Μm7~TtQDx@E<7j+Tm3?3z zz!Hrr`z@a8_;^+Bj3WTHI@P`e^jx<&s{Kjm*~PXZdRyu^0rtpd#iPS^+5}8YTYFxwsKV6 zC^{04T&8a7co7q2tJ=a)gs*$AwqArHX}*fl*II2q>m`tgR%*vzycEu;s(YYz^*#jT zVF9(YHHwMqKI_-weO)3Av^97iQwOA?Pj?PrtnpL_cgJXPd#?_D`Vha@Xsr&Nk4K4Y zrydzK0TbrAdQ|)rfImx-YjC)qPjhYcW;4~J3sB{*+*OYu@mNw@(`s9T_)tBoJ0`TN zk$$(eQq2xjC(ghNq+M>bye6H$)pfJ_Vdf9qUX;2{ zUW|QvOLbidM$Np*>NiI8lv!W(hcyoXyk4sRJ%E1A@2CDAjd^TSqy7=M5c`sz>YrAq z&aEpY+2%C>rdUf!c@M)dXoaNeTnNyAxTJ151dH+zNuBG4!`^VI_1d|3`C%@#`Hq4+ zX^qq&?jv@01Eo$u)&OBGq%OuqSa82eU9&5qo$It+Y~U7fm*5GnrnEK`&J6*C&vq}G$|l?7EW^p#()!yNrxD(dPqSz z_c4z?NJ0Bi9ZlKNz}rVK-~6QD-&UgZe`egUfU)kd6!LT=kRD58z6M--u#(!z)@08U|2!p`X!chBi$doQzW zX~Tb~F*?sk+cRTO0JqUI_Ex3>Ny-e$#A{F@Wgf(qu6Yt;T%xo~`v7l4Jf-Z{==MA! z9bAZI@=&UDa=8{&Bu6@RJqL?ISE*nOI{3DWR45Dq_-%<)xDw+wt(jEhjb_osFi!r) zxN8;Tor6+QTn4C#KE2@=GB=uzb#Su%L#irxg~Hl|vD10!+JHN#l{clDZixUn8>O1Z zV=)Ko=@|zbyGxCw+SD2>LF=Ulc{2h3NlzkCTrRefUiQRYR&JNx<%QuTN@uEZw34k5 zsDq=E%WF;3LszgJ`9sruZ5y=Hb4|-SGo1eyG4|JJ+J>R%PqfuIytT$}kk--^M`LJ` zCN`uUkhs4!)0SdY`}4VGmU}UfUZXVgdX}TEebOY{C@2*1Ru7J^8y*^ZDpX{2Ft-=HK(@ zurckZ`SELGJs)d+K2qYyJdTz*4KzEf)h3ssIBaL!Jd-*&>n$R*`VZ!qdsnp0z0f4f z^0k)!TY%hMO(UEQgEO`EFVpZwdV;ox^<#jOYQ}fY+QIpIFieBB!Scm;_nN02)-MG6 z(jVFg7u?BTZ?%zL z-QNelcDC1U4Bmj>dVJJw&TI#8m+VcC204y#s203ag-tWU(_6lv46$)A1XKnR?v)IT@ba5d|=w=ri zmAOjSY!O~(dwiv3E>Zqjx^7c(W2Mb?p8udO#Fps1-&z4Y9IEqO-yXBIPS^M1NxbE$ z)cH*c!ZE!yWARDGhi!HJA7Ztb#?e`w4E_&wBYR;~>FgM9KhsUTbPdSZB;E9(1MzE{ zOx^4+jd8Zg(k-}~i1D&SxA5k9Yz7N;305kc{wx`{45PK3^yCEn)X6|z(Wb5jm*ct} z&CqaeI>wk>-Hs$T3>eVu%3g-In@x1NHRz6a?R5K+IgEQ19qVc%d+$S2Tn%P_>*^%D zynQ!aSKq5Gj*ZUrjH{t@t?tRa`PjV8)qS3cDZ#JM{cyu0zB{J-8DfSvwV!oAe;)-f zF*BE-f~9%QJT)2X)P*6=zLT#{Ru(QjkMbq4HN+3;5GGWWCCVbjd67 IA(!g@2U0$L+W-In delta 8763 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 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 \