From 4b595e0b55270768f30e651f9dc2fab53f643dd2 Mon Sep 17 00:00:00 2001 From: lh <2334563547@qq.com> Date: Wed, 16 Sep 2026 16:49:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(nmNum):=20=E5=AE=9E=E7=8E=B0=20PEBI=20?= =?UTF-8?q?=E7=BB=93=E6=9E=9C=E5=9C=BA=E6=99=AF=E5=BA=95=E5=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为 MeshLayer 增加通用 VTK 管线输入与事务校验 - 增加主内容边界,隔离装饰图层对相机适配的影响 - 新增 PEBI 结果场景控制器及属性帧原子切换 - 统一 pressure 标量数组映射并更新中文翻译 --- Bin/Config/Lang/cn/nmNum_cn.qm | Bin 140838 -> 141918 bytes Bin/Config/Lang/cn/nmNum_cn.ts | 12 + .../nmPebiResultSnapshotRenderAdapter.h | 2 + .../nmNum/nmRender/nmVTKCameraController.h | 19 +- Include/nmNum/nmRender/nmVTKMeshLayer.h | 51 +- .../nmRender/nmWxVTKRenderContainerWidget.h | 13 +- .../nmSubWxs/nmPebiResultSceneController.h | 75 ++ .../nmPebiResultSnapshotRenderAdapter.cpp | 10 + Src/nmNum/nmRender/nmVTKCameraController.cpp | 87 +- Src/nmNum/nmRender/nmVTKMeshLayer.cpp | 274 ++++- .../nmRender/nmWxVTKRenderContainerWidget.cpp | 16 + .../nmSubWxs/nmPebiResultSceneController.cpp | 1031 +++++++++++++++++ 12 files changed, 1528 insertions(+), 62 deletions(-) create mode 100644 Include/nmNum/nmSubWxs/nmPebiResultSceneController.h create mode 100644 Src/nmNum/nmSubWxs/nmPebiResultSceneController.cpp diff --git a/Bin/Config/Lang/cn/nmNum_cn.qm b/Bin/Config/Lang/cn/nmNum_cn.qm index ee4d0fb16202f69e7415892a117007c1e3ed00bd..eee352b4745cbe4407e3fd9d962478353323a39c 100644 GIT binary patch delta 13313 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 delta 12283 zcmXY%2VBkX7st_v$;zf{C7WEM$cRFeJu^xJg+v); zOER*u`QiWRK7X&5*ZceZcE|Ia=bX&W$Ln%lyk{Yke{s%iz^vBN&m9Gh zAy#u6k%=Y}YaW9KV2`n!FH6BmL`Qr$({e3jy75HfTw+%u!3d&VBfz!bCeHndocCUH zmM#VHxBZ2jZ@*i}Y8~cuoI#O)8kpW!!)+X~S|yy0N#I_h-MfgS_C%2`7P5Mn7w%b$ zKAc^`IlFD=^uEB^?*ZqaFwUSgobxcH^7lu>zUABr`+y@@@-qG)J&5zxf1Ho6a+Wx9 zzAEB;I|1xXbfPwA@q5l6dN7b!z2PAK|5^=>B6db$mK1iTZ3f(C6WfM$l`9id4g$-F zsj)sv7h>8|L{0``o)w9@!yeJiId2*{-xL#hI^a1zx}3>X!GpxQ;%mz3iD8tpM=GZu zzRzbdv8wYeG_gfk#($lNb@L+fwI=%d4aA3i``1ENttDq&2N3HUhxz!f1-pQo!P!K9 zw!}`uz<$e!)rE7G(`gU*2lp#Do#3ebF2N2?AZ+=wMmbHys^i1(fGvuG!|ov3qXMzg zdpdvxM2WX8WXztk5eysv&rZ@<$XGOIBYX~?Gv_VfRATp3}Dfu(vDA^+oQs?nzi>Y8L6UfbSl>L7T80wyNHISY zZA}GNN$QjWSNRQICW^Nr$rJAKeYb@SMk>E&Qv*5Yrjpd(gV;Pbl3-}sHjDGuX_7W! zwc@9P_`LW%VEO0WB`GQsD>aXELIciFdkal$!BYNU(O=F}H93ooB*p$AIu^#6smzLXQbd!{LWa7Rnhv1LTD zK^8Lc5J{OLv1VnQE?%6|eK|M55;rartI&q?0SsGy|1pB32XL?0Y|fKsEMyV}jOXmG z56L#L;HbVNFEcHK4|=aPynWU84YU*1S;pFb((nefSzq%^i7ma~JD zH9ertjutXaEmF4jA*QQ9%Bjoo`T|nU{)qd{;5RUXluP>}EnFofEXvN5k`fkT36)7{ zN-uZv?W6>mzBVb#XM8%CR865VS6Yz%5_~S?3K{-8f?!%f#sSc;4JlM<(<7oMpQ-XK zS0V>1syZqW1Nx6@M8g7xO`H>(bH)Z;igYF`s`okuP$kvv|+kxN%ZYjFo^MfpUvhjRL! zp?3Q^!0B8#*IHBOxIkhK70AOA>X0Q-x1JY?HQs9?&zWtB4b0)(?na)=5v^zOyqph% z$h!&Nkg%1!TVW~Mr*MWn=G;=kx!;%b*hlgXs*7mu$C+7+^IdcDUY0=g{$J`D9YoBl zGN+#@lzNuHq+TA>=l5Ww#@poE2x0ZD9{C<`4B-?xzuYC?)41Pvkn?OE&Wz5SUwc@{ z($cB#LM(|@3};sd>brOau_PNBuns<82;`jbjnfoo$1e(ck$<=gF>)t=^An;SFD+#B znFgi=64@T){I>!P{JI|&@}L04Phv;MQ^06g#(yad8s!Y{*U;cTbBK-mhXyY`L2UYJ z&P`>U$9mJ?E&Yg9$)O=D(#VjVagHOZRB3^>JdJXKCz6lsRdE z7BbZ{&O4iEN;UZW+B!5f*blYT4Vroq27g{i(`;auyA^3#Xg+fJGnzJiCpBTZjx;0k z9nsu}G&6=FD(}&}@(C~8$$5G;g{{mXW_`^(U1Mk3kxy$TBFVUaqs<#cqR^AHTb)Ym zNF>D{6^K$JEM$rj+Uqcf=-NIy&}{;-)B?^&g_P753!A@(PC9oX_PPOGh<7D+xH6@; zhCPd?QP%na#B4utIx@~B33OfEg3NWbRa<-?NjKavq@w$DW6^D5Q|?i2_#@=2YIHj{ z8ewsg3YKY*Lb}icO+2x4LFS2C+iGGx`ndThu~t6x^#8GaV6{@85?#K_>YQnfgq6e^q8?=?Ewe9x0Y)@n4imo+ z1*fr&i<%MT{>wVm55t0;H4o9*nm(;yUH9xFmUo|ZO$Z?h%VxbQA0&4FDD&-%h%EWZ z1}Txu4UO1P+egHDY1qim+o8sj*vPNo$1FD5gu2-24;!<0EU{U4SeVrSqFRM)Q5$^D zgSjmH{6V6Wb)0`jv4|8rpO(v3b~u6Z`O;*zYB%!e$?YsEU?eef8H=8=hA6xP+tDY4 zSdH0iM^aOwwzpX94m@8J$aa0KMC|<=7UvFsu(x9IyMrLEoJ?KB_HQI&H}YB12)ur5 z5<4_FjOg|Tc1#ED%5Tjws-7aY-jQXE+irpfc(a?UIuTp;irx8if!M$6+0!knhze}j zGcl0Zu1~CJMjnd3F{~)<1kstn>{T8-02h~0}& zuoe)}I|T}{YY57hQwnv309bU4LRW5qJ6;NX8`EcE1JV_SF?jIwy~6kvrj+#x8!zOI zQ;QY#223V4^rfPSc{W1-rJ~t3m^?C6(J~hm#;!O;xf-*2cNML-=Mc?0t7y{`D}5k? z^Gh!aS)HMrP7M@odg1Q{s}*k1&4~F-P;_yv8wz>9rRcWo9nt)`oHtAg?_n5mQ@z3` zni1`+VIgCA3STYqR;NTo--m06jp%PaZ?H8*ZBR_P2?J%iDW(=;94jv>LO<7m1dLZq z|8jxokyIEtx&4Ve-ttAoUwwR6+7;yQ9UZjQ|u^8BYN3MQ6A%Lx}-=PULHVq z6p4pWe5P+Pha2ros~0KGDo0|W-gEx>r8wKK6cX&f`L~_o>@e85{V+x9fJ3nR0>#B; z2BL?tii~N&M0L%a0}m>47X>3dk5S~4f=HL6xKld`9;#Q|tC)=9JWX-`D(1Z~M)ByN z50QF;;@QG1GO>>Sis$KSqWh0H|7&9*v)j(u;sj^XT}5&AFT`f(6~(`y4>@BMCFS=; z!xSau_sMj-8W5Nu{{$MZ&l%`*(_aV?>;!#}9zwL(3I#l(V6nkQP>n%->^>f2`! zJMSbkViSp4`wRA=SOFK4VE=j^>YMePukQ;E+7M`kUT{?Vp}5{AI5tMo+3dr4VKwKE zU4o+zK5yk9!Fj<{qQ2vW4#n`;VU-2fRdA%Y`vo`aQh4M}q0=l8D>-?%;NAwFHs=%P zvN)lOT#wjtN1-bQMyKOB({lxnXcu^95{UV|d?0i~$w;pg1y37%;nFXHXEt1@&QHO! zJS6&V7QA*KG-Eaj-mBnZlMe{JKfsmfGw1DwLZ8lykR~@b5PaRM61)0J@Y@fW9;6le z{zf@a_Z(*njo^O?qBNziF!+2da){D=-rCM|%|)1c+>1z&Bg_kelx)5#Y%&KDE&C&E z4#)V-u0rfqNY3%2!mi%&MC;0geGhjNTb(T&kdY;q#|TH#;fd|P3de6jqJ}pTPM7aF zPexeCOohVv_aUfQmkBAK;9yS!h14IziOu&CE+z!Pa5wr17x$Z?&e1~V5G?Q$E8%ix zB(Z8Ig)52Yh<>~fvg3CV-Tfh4n}pXriV^biG0^(M1dI44cNgBcz9V))C%nyjgauFv zAAInIcQ*)MdbdNRDR4UHbGj!8KYEp+VvQHs%`;H@>-R+EO&IP-OHthzt~_zFsO#60 zSlS=X$1_C3!Xre{|8l zU80e0ODc+eJEFfb)luyG77JBxofzO#01bL64#_eh>AxH(4!ISGjol7$=m?nT)qHV; z4c_=FNepdK2=%ZR7Y@cu+FcUEI|iZga7$brFQPD-A#OT5gxKhoVwBf8G<)WAI(Fgg z;?LQ&HK%6>&K_$yeK(3xrt&uq{L3EDipL^WqmiHzPk4@m zjMNrSPD9%!d^(7Mh9447<{OASwuz~eA%~4TIYau2>FSll4ptM>4Yg6?m|Vn6FE3*2 zMvGU{kD)OaWg!cQ<4l<#Ub!4f%%i(_y*)|?TZMStgm81)CFUGhL9Bs|cw@!}G^}ok zx$V-3-3k(O`(Xl~Z-|dyMdtY>z6gYF4XrGeI70to`-^XWe@0DMx=$>9 z1wGmt!})oD_)dC2RK-sG+7CLjbEkz&2p0eJ4@XVZL=xtw6Rl?!vO%7bXsAzAb2ex1 zTapxm0o}U7`PN=iUNfU|NRV_prxWwNELGeGy(?@gRm^LRYA@Z{azW`kIWKqS{LxOb z>DiU&{436{CdmeGpcp;pj}WO^5Tc_`TgmpS8UOSQZaO}#ftwJ$9p`eK$EPkDx1 zTv=+e5Q3EaL2~F*O!R?C&E2l`6R3N)M{d7`+vY~EI-rZfF?^wQ}9Bm<^ zNz$rr5UA)MoOyfAZ>!i<`|c=h>azrTR$bck^BPgbw^GyrxX7@T=B8C`O}BN@?!F#q zHyz^q@4U3<%0@WGBnz3Mk#vCl2eTAQ2TBeT%U&)W3%ibOXq2O6Ph^lT<_PbiJXDH{- zh0^sW2$@4((#=zAh^8-;ZinO{=*y(Ssr9g*%aHCb_=Z+R2kCyfi@(__J)5B?YB@!E zx$P&>tya=2cNCe`?WH%bN|4!FNTr?QiTSma-WK8W)CV~`^pM_%IhnnxS(`F`NdJup zLt+1aKHVtO<7vchua{ZFwy4)z$*N)SHT?|Ej-zF@gdA4AgKQl$0^7Mtvh@Wd)rd86 zja+Q4hW3!_7_bs+3*-jbGl?00ao(RMH=GcHA*eaq*OVLL@9o(~54rJjgmHMBZ0{ow z%daXos{v!@R+ODaAqTyBCA+lxhgg?`ax0rT#N3C;oqc){tK?&@RNc-rdaE3egqXNs z%lS1KjDt%@%0v6)W9yb651kB_$@0+ONbVO8$U%V{p__f>pv)Yku3~w@wE%e3D0xaN zCz!mkJjLx3u@xEeya6a4>M1w_2FuGQA-PxFFRv(X#$4JbuWVfpL2j;KxnP45<&`m^ zsB)IdYYN_B&;L!1Ovcu$dWyV$8f-r!P+lML2pfTEvN^LpvB4WT*S(N8G<^;EJIC4f zg1kv}27;a~Z;H=Cvt}&kgBZ^LeptvF9w;a3+!i1BgYFgNt#7j6Wdk{9*~#06)xpX= zxp1aQ z`{k3zPZN`tgJ;0Y@|oDKSP+GL?vyE(s79=O0o@HY>b{(6haJ@DpYla@8)DbK$Qg;q z5L?D`zHKk(mrf(H)^K*+D&LRJLxDby^Z6|K;R)1Prxo&}F>7Gz$@23y@PLtC@{57n zu;rL3mpn%T*jk12L#+JP)Fc^g@yGIqBO~D&O8LXbX3)(D`O~0pMD~eFxii+R`6W)5 z6wWj2l**rVv9;)?tTqB_vp32@_Hw_n;RXYC*;&fwp8KJ<&y-HVc)wOwI*qD~f?ZO! z*ovhzby2!l!9^FGRyK5*gtcD0R@t^AeDdmEW&1fDArRe_ZZ6Km)~r%?sUAsmZ@2kj zO4$-tJc(UI}BaS*<*JZz_ygNqM@}P4s%EDbo}p zk>^8Y`b^9($yb^F@gVxH=ad;?EwF$4qRdpLqDMAPdHK$BVy)XNZ|C*Gd6kv&&M#Ng zkh#jcLxaeV844{ItneS@y%Ov{j7iD@JtS+IoAQbOZ8*F}S@bZN*w)j^5)Q&q}YjO>u1s$Lf=e_)@g#)&P&dd^bS zQXxA>FXMbupsG#bxX<7$t*oj)2Fi7#gR0>ID3WWMs!=rFf38?%Unc~`dx**j`#C!C zQ{{4N9npYYsHIzm)6jMFDV_3wZ!P@oQ1RQ;C5 z!p$931D+sX&hg-UTthYJz7aL)PR>93RYOYGKx2QX0yTHhhI3F2wT2*=%Q#Ps;VcVN z1)s?wTCqSit>a&!;svUiV~$|oG?sI2sA?ASEENt`&B`;`60=HB&HiDBQc5_Ss16W(IB*~5)*hfjRm7f8TACs&qY|$D8+Xhu(IDGtpmFh`<48%K2^=8Es zq6QACZ_92YJ7=rDdu=2(!AJEYzL?nfAtu$2jBs3-)#7$&YRhM8@dkqEb-Y^4T>_S= zWn1iZt4-kSZLOAPze4xnuv)!EfdQOS>t+=pEf%Zw+2ty;MqTL;qWQ!pb)Bk6r!`uu z>$V}3GOyM3KO1mpa$DUX_Yt-suhh*~bU=%`@eg(DCIiuvRjJ+5QOq?x}!nuu?W%L@?Z1sdQDCAH%0Bg9J_$zQ|h6WcN0r>RS%2pNThO92gxUhZ5hvb z`-XbdwJc(rx~eC5LsojUR8QE2zdxy`o>U3%+tQ3P-(Nkc=VF}dtWi&Qe}pc=YW3`= znAqT*>VfDrWprt^U)goHNva$NwRk zH%R?A>LRh-cQlH37qNDOHS*x|s8Zi*w1H9eiS=8jsnD|vOgo1&`KQL(y#|`i*Em1C zv5=+MYOL`(x;4&1miaf`v?~ZkFuqyrXVv+9o1# z*bm{H@<`Kh@-?_a~`VBdF}w`Cl3o*oobpc|6t`V|1wXvvomRyXh!%T z7Gu9z$kg>U!BdwJZP=>`o>xrl<^@e~?pwruIOmHX&B&8jx5!`%S%8x!ojqv!q6B!p-DJ=kZ7r1lYnJqC!cGQrX7bP8Z`$O?sh?UW~Sy)1VUl4 zzvfg$SLkwe&6ya;WBese>Lq)8(PB-eOFJa`wVaO6&DR^+nLc=FuGVddg7duQdhZST|S7kEu8H-YH}{4WpQKy=dX{NoKm>ORvXTDS2VXWeiKgaHMdhR?(Q!&Pd6eG z4^-B?i3>y^4do1Nr+J5yL3X>V=0jun@3;AyGF2~ZuPP;IzSct9YqFK*+wSr|oY#Dx zU4+J8f|eqX$VOb!vS^gr|7f&A{99sYhG|tb-Lawf)vDaUj<+pj?bTZCOAWS-0i0VG zYmHJp6u1|(#(iC3P(f?F(h&PTqqag22F9*yD>b_eaq7H7TeVgO?nAV7&_pNewVWPx zv<}lti4CctZC1`pcR8O8(K@>y#o>snxoKlt)AxLB*9i#FG2z;-n>V1sVA>wz(@{|0 z;e2;T+cOf5s`ulZaFR3jGUwf7t#5k-o=ch5_vJJEn5MRN;5^Jfu)cQK&~cEo*V^G> zh@&Oh+Mq0)4P11zkjd`a5oyl!4_gvyxnQ{g+F%xjdbYOtA;#74nRa@6Xs~lb?fma+ za4J`{y)1H%l}7D<_CTU6H;Wg$Z5Ow_(_*b5bpRX6RY^-qc1O10k)!!^&R zYkvnpuN)q*ko&6-QURT6V~e*&HjwdWN%%IUUi59s_9xcT!h;8r>;$c$r}yPmb&)q zb`x8brR!h^*Yx|UbGw6?teC9pEUrS0I)U?9q|U%Kl8C&V9gJ-bUdE!wE- zxoSF^Z?!mkp5cr*%9(pY*DLV}G~<)5*CAw4Q-ZGd{o~LUPo1w_|21fGf7AJv^I3D9 z-^*dB0hj86x(p!pVXkh(4OryjSaVGWJCpkx-J~~|$ckpVIa{%m9+P!*#b(GorMd+} zeh@Ve&@J3O6_M~-7d{lF;JtCW@ZjwzfClPTh9i__M(EbtS)o1|tXpqBhE(uUXYN@5 zZF1z;+8*OwM?l7=%Y&qd76Myjs@mkLNO?4Twqp?ko;+z$vyONqk zG|pX@OkF6#1oJV27Yrz>cO z?M32NT|x0kh|&w){dOii*gpjq;H6fa53X6rNWpn&t?pqYOzN~*_w-aK;h(M~5bB;8 zr7P`-8Ry38KAsA|QSMRQ=hSyN=D4a?1T&&eL-ksR)kN)*^!liZsOV(9Rpqfzr{jj{28Q)mn^g|sSmsBvX zZdTRgJKqV1Q~&5kq!(fjEa)fs)h9Ols(#8cRC7yS>!-KO#OdsC{hW?DNJ(Gx3-4Yh zYO`Ozyia9p`Rj3RXvw*^P`_Fqh6ujHxpI(x6YGTlOV)4pS%P1p4CL&StdEj%;NHtk z`W?%l?Jhs{NyV|yxSIMSesIXu9{OWZ0}*&O`eP}`0o~i_lXZbObNZ`K9SBEm6Qs|$ z0_pGiU7vX$)~a$;pVf0BezG}Le=PuC*sOy7X*8@#e){K6@kKj7>fgHK{Y`7>-%o}0 zt6Awkj(dk@vdLFpmJYGb?y3KFVhw)xH%9;cVk)+|P4s`t2iCDz|M$5HjjJMq+!KyG z;JZOpu^7F?FPy(*gSrQ#B=@aB`^*eCP;nlJ4icr3rz z`oUn8hM7Bh8mu#)<2bFY!Dg!(GaO>5{K*>Hb<|Vbofg9l zb@oK#$0>~r_FJ)(!43w8(v8IG#Bn-}Gjynf0ViuYKRO!zP1z4A@HO~Ki;=#woDBoI z`$1gi7y=A%uqZ3T5GPn-?*v1j&KXDgO$5*ce~o z?A6S0qwF9G;2wrN+do7_-^}x!S}C3?%qN`eOx7Bs%_5w*xBY3f{W=}xXiLs>Q#s4r zjP{wX_^C#b(f%2-@}}{ehXu~_6F5ItF}B=zmuOVBvGr7V)Or)=18Za3q6DObcw@WC zm~M6zqbt6LP0BL5f3SrlKI>=n*xUeGTx9HaIT;P3T%+fNUT6W==FDit`DBOD8v|fd z1Y@te*fH%Z1Rb_QUUzw{Z8Y-RRT0b zW1jC^(?q{#7|*$5d>>Mb*-v~>Z&o&5Z2-The%*M@l!F7N_Qr>gzrf$F8jEzdi9Sv> zzUYL|ujptjS&cn+j2q`qZ)3??D7j*xvE=bQ9C|+FeEiw?Efm@$t~CB}ft^3JF#h$k rLObJ|@$abN5W?}k#=py4(W`zAXDaJ@%-$vOTEr!how_6{q8j`UAF;TE diff --git a/Bin/Config/Lang/cn/nmNum_cn.ts b/Bin/Config/Lang/cn/nmNum_cn.ts index 0a82dccf..d9e76056 100644 --- a/Bin/Config/Lang/cn/nmNum_cn.ts +++ b/Bin/Config/Lang/cn/nmNum_cn.ts @@ -2803,6 +2803,18 @@ Reason: %1 Unable to create the grid render layers.无法创建网格渲染图层。 Unable to bind the grid render data.无法绑定网格渲染数据。 + + nmPebiResultSceneController + The result snapshot is invalid.结果快照无效。 + The result snapshot has no renderable property.结果快照中没有可渲染属性。 + Unable to initialize the result render adapter.无法初始化结果渲染适配器。 + The result render data failed validation.结果渲染数据校验失败。 + Unable to create the result render layers.无法创建结果渲染图层。 + Unable to commit the result render data.无法提交结果渲染数据。 + The requested result property or frame is invalid.请求的结果属性或帧无效。 + The result property range is invalid.结果属性范围无效。 + Unable to apply the result property or frame.无法应用结果属性或帧。 + nmWxPebiGridViewWidget No grid data.暂无网格数据。 diff --git a/Include/nmNum/nmData/nmPebiResultSnapshotRenderAdapter.h b/Include/nmNum/nmData/nmPebiResultSnapshotRenderAdapter.h index 3962c23b..19595ed2 100644 --- a/Include/nmNum/nmData/nmPebiResultSnapshotRenderAdapter.h +++ b/Include/nmNum/nmData/nmPebiResultSnapshotRenderAdapter.h @@ -67,6 +67,8 @@ public: int getCurrentFrameIndex() const; /** @brief 返回当前已经绑定的属性 ID,尚未绑定时为空。 */ QString getCurrentPropertyId() const; + /** @brief 返回当前属性对应的稳定 VTK 数组名,尚未绑定时为空。 */ + QString getCurrentScalarArrayName() const; /** @brief 返回适配器渲染网格的单元数。 */ vtkIdType getCellCount() const; /** @brief 返回当前是否使用零整帧复制的共享压力模式。 */ diff --git a/Include/nmNum/nmRender/nmVTKCameraController.h b/Include/nmNum/nmRender/nmVTKCameraController.h index 9b5faf29..d65d33a5 100644 --- a/Include/nmNum/nmRender/nmVTKCameraController.h +++ b/Include/nmNum/nmRender/nmVTKCameraController.h @@ -24,13 +24,18 @@ public: /** @brief 返回当前 Renderer 非所有权指针。 */ vtkRenderer* getRenderer() const; - /** @brief 恢复透视投影并适配当前可见边界。 */ + /** @brief 复制并启用主内容边界;参数有限且各轴最小值不大于最大值。 */ + bool setPrimaryViewBounds(const double dBounds[6]); + /** @brief 清除主内容边界,使相机操作重新使用全部可见 Prop。 */ + void clearPrimaryViewBounds(); + + /** @brief 恢复透视投影并适配主内容或当前可见边界。 */ bool resetCamera(); /** @brief 按固定倍率 1.2 放大当前视图,保持观察方向和投影方式。 */ bool zoomIn(); /** @brief 按固定倍率 1.0 / 1.2 缩小当前视图。 */ bool zoomOut(); - /** @brief 保持当前观察方向和投影方式,仅适配可见边界。 */ + /** @brief 保持当前观察方向和投影方式,仅适配主内容或可见边界。 */ bool fitView(); /** @brief 沿 +Z 方向设置正交顶视图。 */ bool setTopView(); @@ -42,7 +47,7 @@ public: bool setIsometricView(); /** @brief 保存独立世界坐标旋转中心,不修改相机参数。 */ bool setRotationCenter(double dX, double dY, double dZ); - /** @brief 将独立旋转中心恢复为当前可见边界中心,不修改画面。 */ + /** @brief 将独立旋转中心恢复为主内容或可见边界中心,不修改画面。 */ bool resetRotationCenter(); /** @brief 返回是否已经设置有效的独立旋转中心。 */ bool hasRotationCenter() const; @@ -54,9 +59,13 @@ public: private: Q_DISABLE_COPY(nmVTKCameraController) + /** @brief 优先返回主内容边界,未设置时回退到可见 Prop 边界。 */ + bool getViewBounds(double dBounds[6]) const; /** @brief 读取并验证当前可见 Prop 的有限包围盒。 */ bool getVisibleBounds(double dBounds[6]) const; - /** @brief 设置观察方向和投影方式,再按可见边界适配相机。 */ + /** @brief 优先按全部可见 Prop 更新裁剪面,无可见边界时使用构图边界。 */ + void resetClippingRange(const double dViewBounds[6]); + /** @brief 设置观察方向和投影方式,再按主内容或可见边界适配相机。 */ bool applyStandardView(double dDirectionX, double dDirectionY, double dDirectionZ, @@ -68,6 +77,8 @@ private: bool zoom(double dFactor); vtkRenderer* m_pRenderer; ///< 非所有权指针,由渲染容器持有。 + bool m_bHasPrimaryViewBounds; + double m_dPrimaryViewBounds[6]; ///< 由业务控制器提供的主几何边界副本。 bool m_bHasRotationCenter; double m_dRotationCenter[3]; ///< 独立于 vtkCamera::FocalPoint 的世界坐标。 }; diff --git a/Include/nmNum/nmRender/nmVTKMeshLayer.h b/Include/nmNum/nmRender/nmVTKMeshLayer.h index bc41cfd8..0a226257 100644 --- a/Include/nmNum/nmRender/nmVTKMeshLayer.h +++ b/Include/nmNum/nmRender/nmVTKMeshLayer.h @@ -7,6 +7,7 @@ #include class vtkActor; +class vtkAlgorithmOutput; class vtkDataArray; class vtkDataSet; class vtkDataSetMapper; @@ -40,11 +41,29 @@ public: /** * @brief 共享传入数据集的 VTK 引用,不执行 DeepCopy。 - * @return 数据集非空且原标量选择在新数据集中仍有效时返回 true;没有 - * 原标量选择时只要求数据集非空。返回 false 时无效标量状态已清除。 + * @return 数据集非空且原标量选择在新数据集中仍有效时返回 true;失败时 + * 保留原输入、标量选择和 Mapper 状态。传入 nullptr 会清除输入。 */ bool setDataSet(vtkDataSet* pDataSet); - /** @brief 返回当前共享数据集的非所有权指针。 */ + /** + * @brief 连接通用 VTK 管线输出,不取得 Producer 所有权。 + * @return 输出为数据集且原标量选择仍有效时返回 true;失败时保留原输入。 + */ + bool setInputConnection(vtkAlgorithmOutput* pInputConnection); + /** + * @brief 原子提交管线输出、标量数组和显式显示范围。 + * + * 候选输出、数组、数据范围和显示范围会在 Mapper 改动前完整验证; + * 返回 false 时原输入、数组选择、范围和 LookupTable 均保持不变。 + */ + bool setInputConnection(vtkAlgorithmOutput* pInputConnection, + const QString& sArrayName, + ScalarAssociation eAssociation, + double dMinimum, + double dMaximum); + /** @brief 解除直接数据或管线连接,并清除当前标量选择。 */ + void clearInput(); + /** @brief 返回当前直接数据或管线输出数据集的非所有权指针。 */ vtkDataSet* getDataSet() const; /** @brief 设置表面或表面加边线显示模式。 */ @@ -72,6 +91,14 @@ public: */ bool setScalarArray(const QString& sArrayName, ScalarAssociation eAssociation); + /** + * @brief 原子提交当前输入的标量数组和显式显示范围。 + * @return 数组和两个范围均有效时返回 true;失败时保留原 Mapper 状态。 + */ + bool setScalarArray(const QString& sArrayName, + ScalarAssociation eAssociation, + double dMinimum, + double dMaximum); /** @brief 返回当前选中的数组名称。 */ QString getScalarArrayName() const; /** @brief 返回当前数组来自 CellData 还是 PointData。 */ @@ -92,6 +119,23 @@ public: private: Q_DISABLE_COPY(nmVTKMeshLayer) + /** @brief 解析直接输入或当前管线输出;按需先更新上游 Producer。 */ + vtkDataSet* getInputDataSet(bool bUpdatePipeline) const; + /** @brief 在不修改 Mapper 的前提下校验候选数据及保留的标量选择。 */ + bool validateInputDataSet(vtkDataSet* pDataSet, + double dScalarRange[2]) const; + /** @brief 查询指定关联中的数组,并验证其实际数据范围为有限值。 */ + vtkDataArray* validateScalarArray(vtkDataSet* pDataSet, + const QByteArray& aArrayName, + ScalarAssociation eAssociation, + double dDataRange[2]) const; + /** @brief 提交已经完整验证的数组选择和范围;本函数内部没有失败分支。 */ + void applyExplicitScalarState(const QByteArray& aArrayName, + ScalarAssociation eAssociation, + double dMinimum, + double dMaximum); + /** @brief 对新输入重新应用已经验证的标量模式、名称和自动范围。 */ + void applyRetainedScalarState(const double dScalarRange[2]); /** @brief 从当前数据集解析已记录的数组选择。 */ vtkDataArray* getSelectedScalarArray() const; /** @brief 校验数组范围并同步到 Mapper 和 LookupTable。 */ @@ -100,6 +144,7 @@ private: void clearInvalidScalarState(); vtkSmartPointer m_pDataSet; ///< 共享输入数据,不执行深拷贝。 + vtkSmartPointer m_pInputConnection; ///< 共享管线输出端口。 vtkSmartPointer m_pMapper; ///< 图层独占的映射器。 vtkSmartPointer m_pActor; ///< 图层主 Prop,由基类同步持有。 vtkSmartPointer m_pLookupTable; ///< 图层独占的颜色表。 diff --git a/Include/nmNum/nmRender/nmWxVTKRenderContainerWidget.h b/Include/nmNum/nmRender/nmWxVTKRenderContainerWidget.h index 69033976..96af9036 100644 --- a/Include/nmNum/nmRender/nmWxVTKRenderContainerWidget.h +++ b/Include/nmNum/nmRender/nmWxVTKRenderContainerWidget.h @@ -68,6 +68,11 @@ public: void setViewCommandCapabilities( nmVTKViewCommandController::ViewCommands eCommands); + /** @brief 设置相机适配、标准视图和重置中心优先使用的主内容边界。 */ + bool setPrimaryViewBounds(const double dBounds[6]); + /** @brief 清除主内容边界,使相机重新回退到全部可见 Prop 边界。 */ + void clearPrimaryViewBounds(); + /** @brief 注册唯一可拾取几何;传入 nullptr 会取消模式并隐藏中心标记。 */ void setRotationCenterPickTarget(vtkProp* pTarget); /** @brief 返回当前是否正在等待用户选择旋转中心。 */ @@ -90,7 +95,7 @@ public: * 供业务层批量替换几何时使用,调用方应在整批更新结束后统一 renderView。 */ bool fitTopViewWithoutRender(); - /** @brief 将独立旋转中心设为可见边界中心,但不修改画面或立即渲染。 */ + /** @brief 将独立旋转中心设为主内容或可见边界中心,不立即渲染。 */ bool resetRotationCenterWithoutRender(); /** @brief 切换到 VTK 页面并提交一次渲染。 */ void showReadyState(); @@ -115,9 +120,9 @@ public slots: void zoomIn(); /** @brief 按固定倍率 1.0 / 1.2 缩小。 */ void zoomOut(); - /** @brief 保持观察方向和投影方式,仅适配当前可见边界。 */ + /** @brief 保持观察方向和投影方式,仅适配主内容或可见边界。 */ void fitView(); - /** @brief 恢复透视投影,并按当前可见 Prop 的边界适配相机。 */ + /** @brief 恢复透视投影,并按主内容或当前可见 Prop 边界适配相机。 */ void resetCamera(); /** @brief 沿 +Z 方向设置正交顶视图。 */ void setTopView(); @@ -129,7 +134,7 @@ public slots: void setIsometricView(); /** @brief 设置独立世界坐标旋转中心,不移动当前画面。 */ void setRotationCenter(double dX, double dY, double dZ); - /** @brief 将旋转中心恢复为当前可见边界中心,不移动当前画面。 */ + /** @brief 将旋转中心恢复为主内容或当前可见边界中心,不移动画面。 */ void resetRotationCenter(); /** @brief 请求开始或取消一次性旋转中心拾取模式。 */ void setRotationCenterPickingEnabled(bool bEnabled); diff --git a/Include/nmNum/nmSubWxs/nmPebiResultSceneController.h b/Include/nmNum/nmSubWxs/nmPebiResultSceneController.h new file mode 100644 index 00000000..2ed71845 --- /dev/null +++ b/Include/nmNum/nmSubWxs/nmPebiResultSceneController.h @@ -0,0 +1,75 @@ +#ifndef NMPEBIRESULTSCENECONTROLLER_H +#define NMPEBIRESULTSCENECONTROLLER_H + +#include "nmSubWxs_global.h" + +#include +#include +#include + +class nmPebiResultSceneControllerPrivate; +class nmPebiResultSnapshot; +class nmWxVTKRenderContainerWidget; + +/** + * @brief 将不可变 PEBI 结果快照组合为独立的通用 VTK 场景。 + * + * Scene 拥有 Mesh、ScalarBar 和平面比例网格图层;控制器独占活动 + * RenderAdapter 和快照强引用,并通过稳定 ID 校验所观察的图层身份。 + * 外部提前清空业务 Scene 时,控制器会丢弃活动结果而不会解引用旧图层。 + */ +class NM_SUB_WXS_EXPORT nmPebiResultSceneController +{ +public: + /** + * @param pContainer 非所有权指针,必须比控制器生命周期更长。 + * @note 析构会从业务 Scene 移除本控制器创建的稳定 ID 图层。 + */ + explicit nmPebiResultSceneController( + nmWxVTKRenderContainerWidget* pContainer); + ~nmPebiResultSceneController(); + + /** + * @brief 事务式绑定新快照,成功后恢复顶视图、适配网格并渲染一次。 + * @note 候选初始化失败时保留当前 Adapter、画面、范围和相机状态; + * 若发现 Scene 已由外部清空,则采用一致的无快照状态。 + */ + bool setSnapshot( + const QSharedPointer& pSnapshot, + QString* pError = nullptr); + /** @brief 解除管线和快照引用并隐藏结果图层,不销毁 Scene。 */ + void clear(); + + /** + * @brief 原子切换同一快照内的属性和帧,成功时仅渲染一次。 + * @note 静态属性只接受帧 0;失败时恢复 Adapter、Mapper 和范围。 + */ + bool applyPropertyFrame(const QString& sPropertyId, + int nFrameIndex, + QString* pError = nullptr); + + /** 以下显示设置会作为偏好保留,并在存在活动快照时渲染一次。 */ + void setMeshVisible(bool bVisible); + void setScalarBarVisible(bool bVisible); + void setScaleGridVisible(bool bVisible); + void setCellEdgesVisible(bool bVisible); + + bool hasSnapshot() const; + QString getCurrentSnapshotId() const; + QStringList getAvailablePropertyIds() const; + QString getCurrentPropertyId() const; + int getCurrentFrameIndex() const; + /** @brief 返回快照中的业务范围;常量属性保持 [c,c]。 */ + bool getCurrentPropertyRange(double dRange[2]) const; + bool isMeshVisible() const; + bool isScalarBarVisible() const; + bool isScaleGridVisible() const; + bool areCellEdgesVisible() const; + +private: + Q_DISABLE_COPY(nmPebiResultSceneController) + + nmPebiResultSceneControllerPrivate* m_pPrivate; ///< 独占实现对象。 +}; + +#endif // NMPEBIRESULTSCENECONTROLLER_H diff --git a/Src/nmNum/nmData/nmPebiResultSnapshotRenderAdapter.cpp b/Src/nmNum/nmData/nmPebiResultSnapshotRenderAdapter.cpp index abf8a45b..006f883b 100644 --- a/Src/nmNum/nmData/nmPebiResultSnapshotRenderAdapter.cpp +++ b/Src/nmNum/nmData/nmPebiResultSnapshotRenderAdapter.cpp @@ -208,6 +208,16 @@ QString nmPebiResultSnapshotRenderAdapter::getCurrentPropertyId() const return m_sCurrentPropertyId; } +QString nmPebiResultSnapshotRenderAdapter::getCurrentScalarArrayName() const +{ + if(m_sCurrentPropertyId.isEmpty()) + { + return QString(); + } + return m_sCurrentPropertyId == QString::fromLatin1("pressure") + ? QString::fromLatin1("p") : m_sCurrentPropertyId; +} + vtkIdType nmPebiResultSnapshotRenderAdapter::getCellCount() const { return m_pRenderGrid == NULL ? 0 : diff --git a/Src/nmNum/nmRender/nmVTKCameraController.cpp b/Src/nmNum/nmRender/nmVTKCameraController.cpp index b4f3efb6..cff80f07 100644 --- a/Src/nmNum/nmRender/nmVTKCameraController.cpp +++ b/Src/nmNum/nmRender/nmVTKCameraController.cpp @@ -21,9 +21,13 @@ bool isCameraGuiThread() nmVTKCameraController::nmVTKCameraController(vtkRenderer* pRenderer) : m_pRenderer(pRenderer), + m_bHasPrimaryViewBounds(false), m_bHasRotationCenter(false) { Q_ASSERT(isCameraGuiThread()); + for(int nIndex = 0; nIndex < 6; ++nIndex) { + m_dPrimaryViewBounds[nIndex] = 0.0; + } m_dRotationCenter[0] = 0.0; m_dRotationCenter[1] = 0.0; m_dRotationCenter[2] = 0.0; @@ -41,6 +45,7 @@ void nmVTKCameraController::setRenderer(vtkRenderer* pRenderer) Q_ASSERT(isCameraGuiThread()); if(isCameraGuiThread()) { if(m_pRenderer != pRenderer) { + clearPrimaryViewBounds(); clearRotationCenter(); } m_pRenderer = pRenderer; @@ -52,11 +57,43 @@ vtkRenderer* nmVTKCameraController::getRenderer() const return m_pRenderer; } +bool nmVTKCameraController::setPrimaryViewBounds( + const double dBounds[6]) +{ + Q_ASSERT(isCameraGuiThread()); + if(!isCameraGuiThread() || dBounds == nullptr) { + return false; + } + for(int nIndex = 0; nIndex < 6; ++nIndex) { + if(!vtkMath::IsFinite(dBounds[nIndex])) { + return false; + } + } + if(dBounds[0] > dBounds[1] || dBounds[2] > dBounds[3] || + dBounds[4] > dBounds[5]) { + return false; + } + + for(int nIndex = 0; nIndex < 6; ++nIndex) { + m_dPrimaryViewBounds[nIndex] = dBounds[nIndex]; + } + m_bHasPrimaryViewBounds = true; + return true; +} + +void nmVTKCameraController::clearPrimaryViewBounds() +{ + Q_ASSERT(isCameraGuiThread()); + if(isCameraGuiThread()) { + m_bHasPrimaryViewBounds = false; + } +} + bool nmVTKCameraController::resetCamera() { Q_ASSERT(isCameraGuiThread()); double dBounds[6]; - if(!isCameraGuiThread() || !getVisibleBounds(dBounds)) { + if(!isCameraGuiThread() || !getViewBounds(dBounds)) { return false; } @@ -64,10 +101,10 @@ bool nmVTKCameraController::resetCamera() if(pCamera == nullptr) { return false; } - // 重置入口恢复默认三维透视投影,再按当前可见边界适配。 + // 重置入口恢复默认三维透视投影,再按主内容或当前可见边界适配。 pCamera->ParallelProjectionOff(); m_pRenderer->ResetCamera(dBounds); - m_pRenderer->ResetCameraClippingRange(dBounds); + resetClippingRange(dBounds); return true; } @@ -85,7 +122,7 @@ bool nmVTKCameraController::fitView() { Q_ASSERT(isCameraGuiThread()); double dBounds[6]; - if(!isCameraGuiThread() || !getVisibleBounds(dBounds)) { + if(!isCameraGuiThread() || !getViewBounds(dBounds)) { return false; } @@ -96,7 +133,7 @@ bool nmVTKCameraController::fitView() // ResetCamera 只重新适配边界,不改写调用方选定的观察方向和投影方式。 m_pRenderer->ResetCamera(dBounds); - m_pRenderer->ResetCameraClippingRange(dBounds); + resetClippingRange(dBounds); return true; } @@ -148,7 +185,7 @@ bool nmVTKCameraController::resetRotationCenter() { Q_ASSERT(isCameraGuiThread()); double dBounds[6]; - if(!isCameraGuiThread() || !getVisibleBounds(dBounds)) { + if(!isCameraGuiThread() || !getViewBounds(dBounds)) { return false; } // 只更新独立枢轴;当前相机 Position/FocalPoint/ViewUp 保持不变。 @@ -178,6 +215,22 @@ void nmVTKCameraController::clearRotationCenter() m_bHasRotationCenter = false; } +bool nmVTKCameraController::getViewBounds(double dBounds[6]) const +{ + // 主内容边界虽然是值副本,相机操作仍必须依赖有效 Renderer。 + // 这里统一守住空指针,避免设置过主边界后绕过 getVisibleBounds 的校验。 + if(m_pRenderer == nullptr || dBounds == nullptr) { + return false; + } + if(!m_bHasPrimaryViewBounds) { + return getVisibleBounds(dBounds); + } + for(int nIndex = 0; nIndex < 6; ++nIndex) { + dBounds[nIndex] = m_dPrimaryViewBounds[nIndex]; + } + return true; +} + bool nmVTKCameraController::getVisibleBounds(double dBounds[6]) const { if(m_pRenderer == nullptr || dBounds == nullptr) { @@ -199,6 +252,24 @@ bool nmVTKCameraController::getVisibleBounds(double dBounds[6]) const dBounds[4] <= dBounds[5]; } +void nmVTKCameraController::resetClippingRange( + const double dViewBounds[6]) +{ + if(m_pRenderer == nullptr || dViewBounds == nullptr) { + return; + } + double dVisibleBounds[6]; + if(getVisibleBounds(dVisibleBounds)) { + // 裁剪范围可以包含装饰层,但相机位置和缩放仍只由主内容决定。 + m_pRenderer->ResetCameraClippingRange(dVisibleBounds); + } else { + m_pRenderer->ResetCameraClippingRange( + dViewBounds[0], dViewBounds[1], + dViewBounds[2], dViewBounds[3], + dViewBounds[4], dViewBounds[5]); + } +} + bool nmVTKCameraController::applyStandardView( double dDirectionX, double dDirectionY, @@ -210,7 +281,7 @@ bool nmVTKCameraController::applyStandardView( { Q_ASSERT(isCameraGuiThread()); double dBounds[6]; - if(!isCameraGuiThread() || !getVisibleBounds(dBounds)) { + if(!isCameraGuiThread() || !getViewBounds(dBounds)) { return false; } @@ -248,7 +319,7 @@ bool nmVTKCameraController::applyStandardView( pCamera->SetParallelProjection(bParallelProjection ? 1 : 0); pCamera->OrthogonalizeViewUp(); m_pRenderer->ResetCamera(dBounds); - m_pRenderer->ResetCameraClippingRange(dBounds); + resetClippingRange(dBounds); return true; } diff --git a/Src/nmNum/nmRender/nmVTKMeshLayer.cpp b/Src/nmNum/nmRender/nmVTKMeshLayer.cpp index d19153c0..6b88d376 100644 --- a/Src/nmNum/nmRender/nmVTKMeshLayer.cpp +++ b/Src/nmNum/nmRender/nmVTKMeshLayer.cpp @@ -5,8 +5,11 @@ #include #include +#include +#include #include #include +#include #include #include #include @@ -29,6 +32,7 @@ bool isMeshLayerGuiThread() nmVTKMeshLayer::nmVTKMeshLayer(const QString& sLayerId) : nmVTKRenderLayer(sLayerId), m_pDataSet(nullptr), + m_pInputConnection(nullptr), m_pMapper(vtkSmartPointer::New()), m_pActor(vtkSmartPointer::New()), m_pLookupTable(vtkSmartPointer::New()), @@ -56,7 +60,9 @@ nmVTKMeshLayer::~nmVTKMeshLayer() Q_ASSERT(isMeshLayerGuiThread()); // 派生成员释放前先解除 Renderer,基类析构中的 detach 仍保持幂等。 detach(); + clearInput(); m_pDataSet = nullptr; + m_pInputConnection = nullptr; } bool nmVTKMeshLayer::setDataSet(vtkDataSet* pDataSet) @@ -65,41 +71,105 @@ bool nmVTKMeshLayer::setDataSet(vtkDataSet* pDataSet) if(!isMeshLayerGuiThread()) { return false; } + if(pDataSet == nullptr) { + clearInput(); + return false; + } - // vtkSmartPointer 仅增加引用计数,不复制点、单元或属性数组。 - m_pDataSet = pDataSet; + double dScalarRange[2] = { 0.0, 1.0 }; + if(!validateInputDataSet(pDataSet, dScalarRange)) { + return false; + } + + // 所有可能失败的校验均在替换 Mapper 输入前完成。 m_pMapper->SetInputData(pDataSet); + m_pDataSet = pDataSet; + m_pInputConnection = nullptr; + applyRetainedScalarState(dScalarRange); + return true; +} - if(pDataSet == nullptr) { - clearInvalidScalarState(); +bool nmVTKMeshLayer::setInputConnection( + vtkAlgorithmOutput* pInputConnection) +{ + Q_ASSERT(isMeshLayerGuiThread()); + if(!isMeshLayerGuiThread() || pInputConnection == nullptr || + pInputConnection->GetProducer() == nullptr || + pInputConnection->GetIndex() < 0) { return false; } - // 更换数据集时保留调用方此前的数组选择;若同名数组存在则恢复其映射方式。 - if(!m_aScalarArrayName.isEmpty()) { - if(getSelectedScalarArray() == nullptr) { - clearInvalidScalarState(); - return false; - } - if(m_eScalarAssociation == ScalarAssociation_CellData) { - m_pMapper->SetScalarModeToUseCellFieldData(); - } else { - m_pMapper->SetScalarModeToUsePointFieldData(); - } - m_pMapper->SelectColorArray(m_aScalarArrayName.constData()); - if(!m_bUsingExplicitScalarRange) { - if(!applyDataScalarRange()) { - clearInvalidScalarState(); - return false; - } - } + vtkAlgorithm* pProducer = pInputConnection->GetProducer(); + pProducer->Update(pInputConnection->GetIndex()); + vtkDataSet* pCandidateDataSet = vtkDataSet::SafeDownCast( + pProducer->GetOutputDataObject( + pInputConnection->GetIndex())); + double dScalarRange[2] = { 0.0, 1.0 }; + if(!validateInputDataSet(pCandidateDataSet, dScalarRange)) { + return false; } + + // 输出对象、保留数组和自动范围均已验证,此后才替换活动连接。 + m_pMapper->SetInputConnection(pInputConnection); + m_pInputConnection = pInputConnection; + m_pDataSet = nullptr; + applyRetainedScalarState(dScalarRange); + return true; +} + +bool nmVTKMeshLayer::setInputConnection( + vtkAlgorithmOutput* pInputConnection, + const QString& sArrayName, + ScalarAssociation eAssociation, + double dMinimum, + double dMaximum) +{ + Q_ASSERT(isMeshLayerGuiThread()); + if(!isMeshLayerGuiThread() || pInputConnection == nullptr || + pInputConnection->GetProducer() == nullptr || + pInputConnection->GetIndex() < 0 || sArrayName.isEmpty() || + !vtkMath::IsFinite(dMinimum) || !vtkMath::IsFinite(dMaximum) || + dMinimum > dMaximum) { + return false; + } + + vtkAlgorithm* pProducer = pInputConnection->GetProducer(); + const int nOutputPort = pInputConnection->GetIndex(); + pProducer->Update(nOutputPort); + vtkDataSet* pCandidateDataSet = vtkDataSet::SafeDownCast( + pProducer->GetOutputDataObject(nOutputPort)); + const QByteArray aArrayName = sArrayName.toUtf8(); + double dDataRange[2] = { 0.0, 0.0 }; + if(validateScalarArray(pCandidateDataSet, aArrayName, + eAssociation, dDataRange) == nullptr) { + return false; + } + + // 到这里所有可能失败的条件均已排除。下面只执行 VTK 无返回值 setter, + // 因而输入、数组和范围会作为一个不可分割的 Mapper 状态提交。 + m_pMapper->SetInputConnection(pInputConnection); + m_pInputConnection = pInputConnection; + m_pDataSet = nullptr; + applyExplicitScalarState(aArrayName, eAssociation, + dMinimum, dMaximum); return true; } +void nmVTKMeshLayer::clearInput() +{ + Q_ASSERT(isMeshLayerGuiThread()); + if(!isMeshLayerGuiThread() || m_pMapper == nullptr) { + return; + } + m_pMapper->RemoveAllInputs(); + m_pInputConnection = nullptr; + m_pDataSet = nullptr; + clearInvalidScalarState(); +} + vtkDataSet* nmVTKMeshLayer::getDataSet() const { - return m_pDataSet; + return getInputDataSet(false); } void nmVTKMeshLayer::setDisplayMode(DisplayMode eMode) @@ -170,32 +240,20 @@ bool nmVTKMeshLayer::setScalarArray( ScalarAssociation eAssociation) { Q_ASSERT(isMeshLayerGuiThread()); + vtkDataSet* pDataSet = getInputDataSet(true); if(!isMeshLayerGuiThread() || sArrayName.isEmpty() || - m_pDataSet == nullptr || m_pMapper == nullptr || - (eAssociation != ScalarAssociation_CellData && - eAssociation != ScalarAssociation_PointData)) { + pDataSet == nullptr || m_pMapper == nullptr) { return false; } const QByteArray aArrayName = sArrayName.toUtf8(); - vtkDataArray* pArray = eAssociation == ScalarAssociation_CellData - ? m_pDataSet->GetCellData()->GetArray(aArrayName.constData()) - : m_pDataSet->GetPointData()->GetArray(aArrayName.constData()); - if(pArray == nullptr) { + double dRange[2] = { 0.0, 0.0 }; + if(validateScalarArray(pDataSet, aArrayName, + eAssociation, dRange) == nullptr) { // 失败时保留原数组选择和原 Mapper 状态。 return false; } - double dRange[2] = { 0.0, 0.0 }; - if(!m_bUsingExplicitScalarRange) { - pArray->GetRange(dRange); - if(!vtkMath::IsFinite(dRange[0]) || - !vtkMath::IsFinite(dRange[1]) || dRange[0] > dRange[1]) { - // 自动范围无效时不提交新数组选择,避免形成半更新状态。 - return false; - } - } - m_aScalarArrayName = aArrayName; m_eScalarAssociation = eAssociation; if(eAssociation == ScalarAssociation_CellData) { @@ -214,6 +272,34 @@ bool nmVTKMeshLayer::setScalarArray( return true; } +bool nmVTKMeshLayer::setScalarArray( + const QString& sArrayName, + ScalarAssociation eAssociation, + double dMinimum, + double dMaximum) +{ + Q_ASSERT(isMeshLayerGuiThread()); + vtkDataSet* pDataSet = getInputDataSet(true); + if(!isMeshLayerGuiThread() || sArrayName.isEmpty() || + pDataSet == nullptr || m_pMapper == nullptr || + !vtkMath::IsFinite(dMinimum) || !vtkMath::IsFinite(dMaximum) || + dMinimum > dMaximum) { + return false; + } + + const QByteArray aArrayName = sArrayName.toUtf8(); + double dDataRange[2] = { 0.0, 0.0 }; + if(validateScalarArray(pDataSet, aArrayName, + eAssociation, dDataRange) == nullptr) { + return false; + } + + // 数组和范围都已验证,提交阶段不会再产生可观察的半更新状态。 + applyExplicitScalarState(aArrayName, eAssociation, + dMinimum, dMaximum); + return true; +} + QString nmVTKMeshLayer::getScalarArrayName() const { return QString::fromUtf8(m_aScalarArrayName.constData()); @@ -273,15 +359,117 @@ vtkLookupTable* nmVTKMeshLayer::getLookupTable() const return m_pLookupTable; } +vtkDataSet* nmVTKMeshLayer::getInputDataSet(bool bUpdatePipeline) const +{ + if(m_pInputConnection == nullptr) { + return m_pDataSet; + } + + vtkAlgorithm* pProducer = m_pInputConnection->GetProducer(); + const int nOutputPort = m_pInputConnection->GetIndex(); + if(pProducer == nullptr || nOutputPort < 0) { + return nullptr; + } + if(bUpdatePipeline) { + pProducer->Update(nOutputPort); + } + return vtkDataSet::SafeDownCast( + pProducer->GetOutputDataObject(nOutputPort)); +} + +bool nmVTKMeshLayer::validateInputDataSet( + vtkDataSet* pDataSet, + double dScalarRange[2]) const +{ + if(pDataSet == nullptr || dScalarRange == nullptr) { + return false; + } + if(m_aScalarArrayName.isEmpty()) { + return true; + } + + return validateScalarArray(pDataSet, m_aScalarArrayName, + m_eScalarAssociation, + dScalarRange) != nullptr; +} + +vtkDataArray* nmVTKMeshLayer::validateScalarArray( + vtkDataSet* pDataSet, + const QByteArray& aArrayName, + ScalarAssociation eAssociation, + double dDataRange[2]) const +{ + if(pDataSet == nullptr || aArrayName.isEmpty() || + dDataRange == nullptr || + (eAssociation != ScalarAssociation_CellData && + eAssociation != ScalarAssociation_PointData)) { + return nullptr; + } + + vtkDataArray* pArray = eAssociation == ScalarAssociation_CellData + ? pDataSet->GetCellData()->GetArray(aArrayName.constData()) + : pDataSet->GetPointData()->GetArray(aArrayName.constData()); + if(pArray == nullptr) { + return nullptr; + } + pArray->GetRange(dDataRange); + return vtkMath::IsFinite(dDataRange[0]) && + vtkMath::IsFinite(dDataRange[1]) && + dDataRange[0] <= dDataRange[1] + ? pArray : nullptr; +} + +void nmVTKMeshLayer::applyExplicitScalarState( + const QByteArray& aArrayName, + ScalarAssociation eAssociation, + double dMinimum, + double dMaximum) +{ + m_aScalarArrayName = aArrayName; + m_eScalarAssociation = eAssociation; + if(eAssociation == ScalarAssociation_CellData) { + m_pMapper->SetScalarModeToUseCellFieldData(); + } else { + m_pMapper->SetScalarModeToUsePointFieldData(); + } + m_pMapper->SelectColorArray(m_aScalarArrayName.constData()); + m_pMapper->SetScalarRange(dMinimum, dMaximum); + m_pLookupTable->SetRange(dMinimum, dMaximum); + m_pLookupTable->Build(); + m_bUsingExplicitScalarRange = true; +} + +void nmVTKMeshLayer::applyRetainedScalarState( + const double dScalarRange[2]) +{ + if(m_aScalarArrayName.isEmpty()) { + return; + } + if(m_eScalarAssociation == ScalarAssociation_CellData) { + m_pMapper->SetScalarModeToUseCellFieldData(); + } else { + m_pMapper->SetScalarModeToUsePointFieldData(); + } + m_pMapper->SelectColorArray(m_aScalarArrayName.constData()); + if(!m_bUsingExplicitScalarRange && dScalarRange != nullptr) { + m_pMapper->SetScalarRange( + dScalarRange[0], dScalarRange[1]); + m_pLookupTable->SetRange( + dScalarRange[0], dScalarRange[1]); + m_pLookupTable->Build(); + } +} + vtkDataArray* nmVTKMeshLayer::getSelectedScalarArray() const { - if(m_pDataSet == nullptr || m_aScalarArrayName.isEmpty()) { + vtkDataSet* pDataSet = getInputDataSet(true); + if(pDataSet == nullptr || m_aScalarArrayName.isEmpty()) { return nullptr; } return m_eScalarAssociation == ScalarAssociation_CellData - ? m_pDataSet->GetCellData()->GetArray( + ? pDataSet->GetCellData()->GetArray( m_aScalarArrayName.constData()) - : m_pDataSet->GetPointData()->GetArray( + : pDataSet->GetPointData()->GetArray( m_aScalarArrayName.constData()); } diff --git a/Src/nmNum/nmRender/nmWxVTKRenderContainerWidget.cpp b/Src/nmNum/nmRender/nmWxVTKRenderContainerWidget.cpp index df02ab39..c00c673b 100644 --- a/Src/nmNum/nmRender/nmWxVTKRenderContainerWidget.cpp +++ b/Src/nmNum/nmRender/nmWxVTKRenderContainerWidget.cpp @@ -288,6 +288,22 @@ void nmWxVTKRenderContainerWidget::setViewCommandCapabilities( updateRotationCenterMarkerVisibility(); } +bool nmWxVTKRenderContainerWidget::setPrimaryViewBounds( + const double dBounds[6]) +{ + Q_ASSERT(isGuiThread()); + return isGuiThread() && m_pCameraController != nullptr && + m_pCameraController->setPrimaryViewBounds(dBounds); +} + +void nmWxVTKRenderContainerWidget::clearPrimaryViewBounds() +{ + Q_ASSERT(isGuiThread()); + if(isGuiThread() && m_pCameraController != nullptr) { + m_pCameraController->clearPrimaryViewBounds(); + } +} + void nmWxVTKRenderContainerWidget::setRotationCenterPickTarget( vtkProp* pTarget) { diff --git a/Src/nmNum/nmSubWxs/nmPebiResultSceneController.cpp b/Src/nmNum/nmSubWxs/nmPebiResultSceneController.cpp new file mode 100644 index 00000000..b5168c43 --- /dev/null +++ b/Src/nmNum/nmSubWxs/nmPebiResultSceneController.cpp @@ -0,0 +1,1031 @@ +#include "nmPebiResultSceneController.h" + +#include "nmPebiResultPropertyCatalog.h" +#include "nmPebiResultSnapshot.h" +#include "nmPebiResultSnapshotRenderAdapter.h" + +#include "nmVTKMeshLayer.h" +#include "nmVTKPlanarScaleGridLayer.h" +#include "nmVTKScalarBarLayer.h" +#include "nmVTKScene.h" +#include "nmWxVTKRenderContainerWidget.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace { + +const char* const RESULT_MESH_LAYER_ID = "pebi.result.mesh"; +const char* const RESULT_SCALAR_BAR_LAYER_ID = "pebi.result.scalar_bar"; +const char* const RESULT_SCALE_GRID_LAYER_ID = "pebi.result.scale_grid"; +const char* const RESULT_WELLS_LAYER_ID = "pebi.result.wells"; + +/* Qt 4.8 的 lupdate 无法从变量参数中推导翻译原文,必须显式登记。 */ +static const char* const s_pResultSceneTranslationMarkers[] = +{ + QT_TRANSLATE_NOOP("nmPebiResultSceneController", + "The result snapshot is invalid."), + QT_TRANSLATE_NOOP("nmPebiResultSceneController", + "The result snapshot has no renderable property."), + QT_TRANSLATE_NOOP("nmPebiResultSceneController", + "Unable to initialize the result render adapter."), + QT_TRANSLATE_NOOP("nmPebiResultSceneController", + "The result render data failed validation."), + QT_TRANSLATE_NOOP("nmPebiResultSceneController", + "Unable to create the result render layers."), + QT_TRANSLATE_NOOP("nmPebiResultSceneController", + "Unable to commit the result render data."), + QT_TRANSLATE_NOOP("nmPebiResultSceneController", + "The requested result property or frame is invalid."), + QT_TRANSLATE_NOOP("nmPebiResultSceneController", + "The result property range is invalid."), + QT_TRANSLATE_NOOP("nmPebiResultSceneController", + "Unable to apply the result property or frame.") +}; + +bool isResultSceneGuiThread() +{ + QCoreApplication* pApplication = QCoreApplication::instance(); + return pApplication == nullptr || + pApplication->thread() == QThread::currentThread(); +} + +QString controllerText(const char* pText) +{ + Q_UNUSED(s_pResultSceneTranslationMarkers); + return QCoreApplication::translate( + "nmPebiResultSceneController", pText); +} + +void copyBounds(const double dSource[6], double dTarget[6]) +{ + for(int nIndex = 0; nIndex < 6; ++nIndex) { + dTarget[nIndex] = dSource[nIndex]; + } +} + +bool getFiniteBounds(vtkDataSet* pDataSet, double dBounds[6]) +{ + if(pDataSet == nullptr || dBounds == nullptr) { + return false; + } + pDataSet->GetBounds(dBounds); + if(!vtkMath::AreBoundsInitialized(dBounds)) { + return false; + } + for(int nIndex = 0; nIndex < 6; ++nIndex) { + if(!vtkMath::IsFinite(dBounds[nIndex])) { + return false; + } + } + return dBounds[0] < dBounds[1] && + dBounds[2] < dBounds[3] && + dBounds[4] <= dBounds[5]; +} + +bool getDisplayRange(const double dBusinessRange[2], + double dDisplayRange[2]) +{ + if(dBusinessRange == nullptr || dDisplayRange == nullptr || + !vtkMath::IsFinite(dBusinessRange[0]) || + !vtkMath::IsFinite(dBusinessRange[1]) || + dBusinessRange[0] > dBusinessRange[1]) { + return false; + } + + dDisplayRange[0] = dBusinessRange[0]; + dDisplayRange[1] = dBusinessRange[1]; + if(dBusinessRange[0] < dBusinessRange[1]) { + return true; + } + + // VTK 需要非零显示区间;业务层仍保存并返回原始常量范围 [c,c]。 + const double dValue = dBusinessRange[0]; + const double dEpsilon = qMax(1.0, fabs(dValue)) * 1.0e-6; + double dMinimum = dValue - dEpsilon; + double dMaximum = dValue + dEpsilon; + if(!vtkMath::IsFinite(dMinimum)) { + dMinimum = dValue; + } + if(!vtkMath::IsFinite(dMaximum)) { + dMaximum = dValue; + } + if(dMinimum >= dMaximum) { + return false; + } + dDisplayRange[0] = dMinimum; + dDisplayRange[1] = dMaximum; + return true; +} + +bool getValidatedPropertyRanges( + const QSharedPointer& pSnapshot, + const QString& sPropertyId, + double dBusinessRange[2], + double dDisplayRange[2]) +{ + return !pSnapshot.isNull() && + pSnapshot->getPropertyRange(sPropertyId, dBusinessRange) && + getDisplayRange(dBusinessRange, dDisplayRange); +} + +QString chooseInitialProperty( + const QSharedPointer& pSnapshot) +{ + const QString sPressure = QString::fromLatin1("pressure"); + if(!pSnapshot.isNull() && pSnapshot->hasProperty(sPressure) && + pSnapshot->getPropertyFrameCount(sPressure) > 0) { + return sPressure; + } + + // 无压力仅是防御分支;按 Catalog 顺序选择第一个可用静态属性。 + const QList listDescriptors = + nmPebiResultPropertyCatalog::descriptors(); + for(int nIndex = 0; nIndex < listDescriptors.size(); ++nIndex) { + const nmPebiResultPropertyDescriptor& oDescriptor = + listDescriptors[nIndex]; + if(oDescriptor.m_eTimeType == NM_PebiResultProperty_Static && + pSnapshot->hasProperty(oDescriptor.m_sPropertyId)) { + return oDescriptor.m_sPropertyId; + } + } + return QString(); +} + +bool areSnapshotWellsValid( + const QSharedPointer& pSnapshot) +{ + if(pSnapshot.isNull()) { + return false; + } + QSet setWellIds; + for(int nIndex = 0; nIndex < pSnapshot->getWellCount(); ++nIndex) { + const nmPebiResultWellSnapshot* pWell = pSnapshot->getWellAt(nIndex); + if(pWell == nullptr || pWell->m_sWellInstanceId.isEmpty() || + pWell->m_sWellCode.isEmpty() || + !vtkMath::IsFinite(pWell->m_oLocation.x()) || + !vtkMath::IsFinite(pWell->m_oLocation.y()) || + setWellIds.contains(pWell->m_sWellInstanceId)) { + return false; + } + setWellIds.insert(pWell->m_sWellInstanceId); + } + return true; +} + +bool validateAdapterOutput( + nmPebiResultSnapshotRenderAdapter* pAdapter, + vtkIdType nExpectedCellCount, + double dBounds[6]) +{ + if(pAdapter == nullptr || dBounds == nullptr || + pAdapter->getCellCount() != nExpectedCellCount || + nExpectedCellCount <= 0) { + return false; + } + + vtkAlgorithmOutput* pOutputPort = pAdapter->getOutputPort(); + if(pOutputPort == nullptr || pOutputPort->GetProducer() == nullptr || + pOutputPort->GetIndex() < 0) { + return false; + } + vtkAlgorithm* pProducer = pOutputPort->GetProducer(); + pProducer->Update(pOutputPort->GetIndex()); + vtkDataSet* pDataSet = vtkDataSet::SafeDownCast( + pProducer->GetOutputDataObject(pOutputPort->GetIndex())); + if(pDataSet == nullptr || + pDataSet->GetNumberOfCells() != nExpectedCellCount || + !getFiniteBounds(pDataSet, dBounds)) { + return false; + } + + const QByteArray aScalarName = + pAdapter->getCurrentScalarArrayName().toUtf8(); + vtkDataArray* pScalars = aScalarName.isEmpty() ? nullptr : + pDataSet->GetCellData()->GetArray(aScalarName.constData()); + if(pScalars == nullptr || pScalars->GetNumberOfComponents() != 1 || + pScalars->GetNumberOfTuples() != nExpectedCellCount) { + return false; + } + double dActualRange[2] = { 0.0, 0.0 }; + pScalars->GetRange(dActualRange); + return vtkMath::IsFinite(dActualRange[0]) && + vtkMath::IsFinite(dActualRange[1]) && + dActualRange[0] <= dActualRange[1]; +} + +} + +class nmPebiResultSceneControllerPrivate +{ +public: + explicit nmPebiResultSceneControllerPrivate( + nmWxVTKRenderContainerWidget* pContainer) + : m_pContainer(pContainer), + m_pMeshLayer(nullptr), + m_pScalarBarLayer(nullptr), + m_pScaleGridLayer(nullptr), + m_pAdapter(nullptr), + m_nCurrentFrameIndex(-1), + m_bHasPropertyRange(false), + m_bHasBounds(false), + m_bMeshVisible(true), + // 5C 补齐属性名和单位标题前,避免显示语义不完整的色标。 + m_bScalarBarVisible(false), + m_bScaleGridVisible(false), + m_bCellEdgesVisible(false) + { + m_dBusinessRange[0] = 0.0; + m_dBusinessRange[1] = 0.0; + m_dDisplayRange[0] = 0.0; + m_dDisplayRange[1] = 1.0; + for(int nIndex = 0; nIndex < 6; ++nIndex) { + m_dBounds[nIndex] = 0.0; + } + // 5A 固定保留该 ID,井位图层在 5C 才创建。 + Q_UNUSED(RESULT_WELLS_LAYER_ID); + } + + nmVTKScene* getScene() const + { + return m_pContainer != nullptr ? m_pContainer->getScene() : nullptr; + } + + bool synchronizeLayerPointers() + { + nmVTKScene* pScene = getScene(); + bool bPointersIntact = pScene != nullptr; + if(pScene == nullptr) { + bPointersIntact = m_pMeshLayer == nullptr && + m_pScalarBarLayer == nullptr && + m_pScaleGridLayer == nullptr; + m_pMeshLayer = nullptr; + m_pScalarBarLayer = nullptr; + m_pScaleGridLayer = nullptr; + return bPointersIntact; + } + + // Scene 是图层唯一所有者。这里只比较指针身份,不解引用可能已经由 + // 外部 Scene::clear() 删除的旧地址;不一致时立即丢弃观察指针。 + if(m_pMeshLayer != nullptr && + pScene->getLayer(QString::fromLatin1(RESULT_MESH_LAYER_ID)) != + m_pMeshLayer) { + m_pMeshLayer = nullptr; + bPointersIntact = false; + } + if(m_pScalarBarLayer != nullptr && + pScene->getLayer( + QString::fromLatin1(RESULT_SCALAR_BAR_LAYER_ID)) != + m_pScalarBarLayer) { + m_pScalarBarLayer = nullptr; + bPointersIntact = false; + } + if(m_pScaleGridLayer != nullptr && + pScene->getLayer( + QString::fromLatin1(RESULT_SCALE_GRID_LAYER_ID)) != + m_pScaleGridLayer) { + m_pScaleGridLayer = nullptr; + bPointersIntact = false; + } + return bPointersIntact; + } + + bool hasOwnedBaseLayers() const + { + if(!isResultSceneGuiThread()) { + return false; + } + nmVTKScene* pScene = getScene(); + return pScene != nullptr && + m_pMeshLayer != nullptr && + m_pScalarBarLayer != nullptr && + m_pScaleGridLayer != nullptr && + pScene->getLayer( + QString::fromLatin1(RESULT_MESH_LAYER_ID)) == + m_pMeshLayer && + pScene->getLayer( + QString::fromLatin1(RESULT_SCALAR_BAR_LAYER_ID)) == + m_pScalarBarLayer && + pScene->getLayer( + QString::fromLatin1(RESULT_SCALE_GRID_LAYER_ID)) == + m_pScaleGridLayer; + } + + void discardActiveDataIfLayersLost() + { + const bool bPointersIntact = synchronizeLayerPointers(); + const bool bHasActiveState = m_pAdapter != nullptr || + !m_pSnapshot.isNull(); + if(bHasActiveState && + (!bPointersIntact || !hasOwnedBaseLayers())) { + // 外部已经清理业务 Scene 时无法继续兑现“当前快照可见”的状态, + // 因此采用 fail-closed:释放逻辑状态,但绝不访问已删除的 Layer。 + releaseActiveData(); + } + } + + bool ensureBaseLayers(bool& bCreatedMesh, + bool& bCreatedScalarBar, + bool& bCreatedScaleGrid) + { + bCreatedMesh = false; + bCreatedScalarBar = false; + bCreatedScaleGrid = false; + const bool bPointersIntact = synchronizeLayerPointers(); + nmVTKScene* pScene = getScene(); + vtkRenderer* pRenderer = m_pContainer != nullptr + ? m_pContainer->getRenderer() : nullptr; + if(pScene == nullptr || pRenderer == nullptr || + (!bPointersIntact && m_pAdapter != nullptr)) { + return false; + } + + if(m_pMeshLayer == nullptr) { + if(pScene->getLayer( + QString::fromLatin1(RESULT_MESH_LAYER_ID)) != nullptr) { + return false; + } + nmVTKMeshLayer* pLayer = new nmVTKMeshLayer( + QString::fromLatin1(RESULT_MESH_LAYER_ID)); + pLayer->setVisible(false); + if(!pScene->addLayer(pLayer)) { + delete pLayer; + return false; + } + m_pMeshLayer = pLayer; + bCreatedMesh = true; + m_pMeshLayer->getActor()->GetProperty()->SetEdgeColor( + 0.0, 0.0, 0.0); + m_pMeshLayer->getActor()->GetProperty()->LightingOff(); + } + + if(m_pScalarBarLayer == nullptr) { + if(pScene->getLayer(QString::fromLatin1( + RESULT_SCALAR_BAR_LAYER_ID)) != nullptr) { + rollbackCreatedLayers( + bCreatedMesh, false, false); + return false; + } + nmVTKScalarBarLayer* pLayer = new nmVTKScalarBarLayer( + QString::fromLatin1(RESULT_SCALAR_BAR_LAYER_ID)); + pLayer->setVisible(false); + pLayer->setPosition(0.05, 0.1); + pLayer->setSize(0.1, 0.8); + if(!pScene->addLayer(pLayer)) { + delete pLayer; + rollbackCreatedLayers( + bCreatedMesh, false, false); + return false; + } + m_pScalarBarLayer = pLayer; + bCreatedScalarBar = true; + } + + if(m_pScaleGridLayer == nullptr) { + if(pScene->getLayer(QString::fromLatin1( + RESULT_SCALE_GRID_LAYER_ID)) != nullptr) { + rollbackCreatedLayers( + bCreatedMesh, bCreatedScalarBar, false); + return false; + } + nmVTKPlanarScaleGridLayer* pLayer = + new nmVTKPlanarScaleGridLayer( + QString::fromLatin1(RESULT_SCALE_GRID_LAYER_ID)); + pLayer->setVisible(false); + if(!pScene->addLayer(pLayer)) { + delete pLayer; + rollbackCreatedLayers( + bCreatedMesh, bCreatedScalarBar, false); + return false; + } + m_pScaleGridLayer = pLayer; + bCreatedScaleGrid = true; + } + return true; + } + + void rollbackCreatedLayers(bool bCreatedMesh, + bool bCreatedScalarBar, + bool bCreatedScaleGrid) + { + synchronizeLayerPointers(); + nmVTKScene* pScene = getScene(); + if(pScene == nullptr) { + return; + } + if(bCreatedScaleGrid && m_pScaleGridLayer != nullptr && + pScene->getLayer( + QString::fromLatin1(RESULT_SCALE_GRID_LAYER_ID)) == + m_pScaleGridLayer) { + pScene->removeLayer( + QString::fromLatin1(RESULT_SCALE_GRID_LAYER_ID)); + } + if(bCreatedScaleGrid) { + m_pScaleGridLayer = nullptr; + } + if(bCreatedScalarBar && m_pScalarBarLayer != nullptr && + pScene->getLayer( + QString::fromLatin1(RESULT_SCALAR_BAR_LAYER_ID)) == + m_pScalarBarLayer) { + pScene->removeLayer( + QString::fromLatin1(RESULT_SCALAR_BAR_LAYER_ID)); + } + if(bCreatedScalarBar) { + m_pScalarBarLayer = nullptr; + } + if(bCreatedMesh && m_pMeshLayer != nullptr && + pScene->getLayer(QString::fromLatin1(RESULT_MESH_LAYER_ID)) == + m_pMeshLayer) { + pScene->removeLayer( + QString::fromLatin1(RESULT_MESH_LAYER_ID)); + } + if(bCreatedMesh) { + m_pMeshLayer = nullptr; + } + } + + bool bindMeshAdapter(nmPebiResultSnapshotRenderAdapter* pAdapter, + const double dDisplayRange[2]) + { + if(m_pMeshLayer == nullptr || pAdapter == nullptr || + dDisplayRange == nullptr) { + return false; + } + + // MeshLayer 在触碰旧 Mapper 前完成全部校验,失败不会破坏旧画面。 + return m_pMeshLayer->setInputConnection( + pAdapter->getOutputPort(), + pAdapter->getCurrentScalarArrayName(), + nmVTKMeshLayer::ScalarAssociation_CellData, + dDisplayRange[0], dDisplayRange[1]); + } + + void applyLayerVisibility(bool bHasData) + { + synchronizeLayerPointers(); + if(m_pMeshLayer != nullptr) { + m_pMeshLayer->setDisplayMode(m_bCellEdgesVisible + ? nmVTKMeshLayer::DisplayMode_SurfaceWithEdges + : nmVTKMeshLayer::DisplayMode_Surface); + m_pMeshLayer->setScalarVisibility(bHasData); + m_pMeshLayer->setVisible(bHasData && m_bMeshVisible); + } + if(m_pScalarBarLayer != nullptr) { + m_pScalarBarLayer->setVisible( + bHasData && m_bScalarBarVisible); + } + if(m_pScaleGridLayer != nullptr) { + m_pScaleGridLayer->setVisible( + bHasData && m_bScaleGridVisible); + } + if(m_pContainer != nullptr) { + m_pContainer->setRotationCenterPickTarget( + bHasData && m_bMeshVisible && m_pMeshLayer != nullptr + ? m_pMeshLayer->getActor() : nullptr); + } + } + + bool restorePreviousScene() + { + synchronizeLayerPointers(); + if(!hasOwnedBaseLayers() || m_pContainer == nullptr) { + return false; + } + if(m_pAdapter == nullptr || m_pSnapshot.isNull()) { + m_pMeshLayer->clearInput(); + m_pContainer->clearPrimaryViewBounds(); + if(m_pScalarBarLayer != nullptr) { + m_pScalarBarLayer->setLookupTable(nullptr); + m_pScalarBarLayer->setTitle(QString()); + } + applyLayerVisibility(false); + return true; + } + + const bool bMeshRestored = bindMeshAdapter( + m_pAdapter, m_dDisplayRange); + bool bGridRestored = !m_bHasBounds; + bool bBoundsRestored = !m_bHasBounds; + if(m_bHasBounds && m_pScaleGridLayer != nullptr) { + bGridRestored = m_pScaleGridLayer->setBounds( + m_dBounds[0], m_dBounds[1], + m_dBounds[2], m_dBounds[3], m_dBounds[4]); + bBoundsRestored = m_pContainer->setPrimaryViewBounds(m_dBounds); + } + if(m_pScalarBarLayer != nullptr) { + m_pScalarBarLayer->setLookupTable( + m_pMeshLayer->getLookupTable()); + } + applyLayerVisibility(true); + return bMeshRestored && bGridRestored && bBoundsRestored; + } + + void releaseActiveData() + { + // Scene 可能已由外部清空;先按稳定 ID 解除所有悬空观察指针。 + synchronizeLayerPointers(); + if(m_pContainer != nullptr) { + m_pContainer->setRotationCenterPickTarget(nullptr); + m_pContainer->clearPrimaryViewBounds(); + } + if(m_pMeshLayer != nullptr) { + m_pMeshLayer->setScalarVisibility(false); + m_pMeshLayer->clearInput(); + m_pMeshLayer->setVisible(false); + } + if(m_pScalarBarLayer != nullptr) { + m_pScalarBarLayer->setVisible(false); + m_pScalarBarLayer->setLookupTable(nullptr); + m_pScalarBarLayer->setTitle(QString()); + } + if(m_pScaleGridLayer != nullptr) { + m_pScaleGridLayer->setVisible(false); + } + delete m_pAdapter; + m_pAdapter = nullptr; + m_pSnapshot.clear(); + m_listAvailablePropertyIds.clear(); + m_sCurrentPropertyId.clear(); + m_nCurrentFrameIndex = -1; + m_bHasPropertyRange = false; + m_bHasBounds = false; + } + + void removeOwnedLayers() + { + synchronizeLayerPointers(); + nmVTKScene* pScene = getScene(); + if(pScene == nullptr) { + return; + } + + // 只删除仍与本控制器记录身份一致的图层,绝不误删同 ID 的外部替代层。 + if(m_pScaleGridLayer != nullptr && + pScene->getLayer( + QString::fromLatin1(RESULT_SCALE_GRID_LAYER_ID)) == + m_pScaleGridLayer) { + pScene->removeLayer( + QString::fromLatin1(RESULT_SCALE_GRID_LAYER_ID)); + } + m_pScaleGridLayer = nullptr; + if(m_pScalarBarLayer != nullptr && + pScene->getLayer( + QString::fromLatin1(RESULT_SCALAR_BAR_LAYER_ID)) == + m_pScalarBarLayer) { + pScene->removeLayer( + QString::fromLatin1(RESULT_SCALAR_BAR_LAYER_ID)); + } + m_pScalarBarLayer = nullptr; + if(m_pMeshLayer != nullptr && + pScene->getLayer(QString::fromLatin1(RESULT_MESH_LAYER_ID)) == + m_pMeshLayer) { + pScene->removeLayer( + QString::fromLatin1(RESULT_MESH_LAYER_ID)); + } + m_pMeshLayer = nullptr; + } + + nmWxVTKRenderContainerWidget* m_pContainer; ///< 非所有权指针。 + nmVTKMeshLayer* m_pMeshLayer; ///< Scene 所有。 + nmVTKScalarBarLayer* m_pScalarBarLayer; ///< Scene 所有。 + nmVTKPlanarScaleGridLayer* m_pScaleGridLayer; ///< Scene 所有。 + nmPebiResultSnapshotRenderAdapter* m_pAdapter; ///< 控制器独占。 + QSharedPointer m_pSnapshot; + QStringList m_listAvailablePropertyIds; + QString m_sCurrentPropertyId; + int m_nCurrentFrameIndex; + double m_dBusinessRange[2]; + double m_dDisplayRange[2]; + double m_dBounds[6]; + bool m_bHasPropertyRange; + bool m_bHasBounds; + bool m_bMeshVisible; + bool m_bScalarBarVisible; + bool m_bScaleGridVisible; + bool m_bCellEdgesVisible; +}; + +nmPebiResultSceneController::nmPebiResultSceneController( + nmWxVTKRenderContainerWidget* pContainer) + : m_pPrivate(new nmPebiResultSceneControllerPrivate(pContainer)) +{ + Q_ASSERT(isResultSceneGuiThread()); +} + +nmPebiResultSceneController::~nmPebiResultSceneController() +{ + Q_ASSERT(isResultSceneGuiThread()); + if(m_pPrivate != nullptr) { + m_pPrivate->releaseActiveData(); + // Controller 结束后立即移除自己的业务图层,使同一容器可安全创建新实例。 + m_pPrivate->removeOwnedLayers(); + m_pPrivate->m_pContainer = nullptr; + delete m_pPrivate; + m_pPrivate = nullptr; + } +} + +bool nmPebiResultSceneController::setSnapshot( + const QSharedPointer& pSnapshot, + QString* pError) +{ + Q_ASSERT(isResultSceneGuiThread()); + if(pError != nullptr) { + pError->clear(); + } + if(!isResultSceneGuiThread() || m_pPrivate == nullptr || + m_pPrivate->m_pContainer == nullptr || pSnapshot.isNull() || + !pSnapshot->isComplete()) { + if(pError != nullptr) { + *pError = controllerText("The result snapshot is invalid."); + } + return false; + } + m_pPrivate->discardActiveDataIfLayersLost(); + if(m_pPrivate->m_pSnapshot == pSnapshot && + m_pPrivate->m_pSnapshot->getSnapshotId() == + pSnapshot->getSnapshotId()) { + return true; + } + + const QString sInitialProperty = chooseInitialProperty(pSnapshot); + nmPebiResultPropertyDescriptor oInitialDescriptor; + if(sInitialProperty.isEmpty() || + !nmPebiResultPropertyCatalog::findDescriptor( + sInitialProperty, oInitialDescriptor) || + oInitialDescriptor.m_eAssociation != + NM_PebiResultPropertyAssociation_CellData) { + if(pError != nullptr) { + *pError = controllerText( + "The result snapshot has no renderable property."); + } + return false; + } + + nmPebiResultSnapshotRenderAdapter* pCandidateAdapter = + new nmPebiResultSnapshotRenderAdapter(); + if(!pCandidateAdapter->initialize( + pSnapshot, + nmPebiResultSnapshotRenderAdapter::SharedPropertyArray) || + !pCandidateAdapter->setPropertyFrame(sInitialProperty, 0)) { + delete pCandidateAdapter; + if(pError != nullptr) { + *pError = controllerText( + "Unable to initialize the result render adapter."); + } + return false; + } + + double dBusinessRange[2]; + double dDisplayRange[2]; + double dBounds[6]; + if(pCandidateAdapter->getCurrentScalarArrayName().isEmpty() || + !getValidatedPropertyRanges(pSnapshot, sInitialProperty, + dBusinessRange, dDisplayRange) || + !validateAdapterOutput(pCandidateAdapter, + pSnapshot->getResultCellCount(), dBounds) || + !areSnapshotWellsValid(pSnapshot)) { + delete pCandidateAdapter; + if(pError != nullptr) { + *pError = controllerText( + "The result render data failed validation."); + } + return false; + } + + bool bCreatedMesh = false; + bool bCreatedScalarBar = false; + bool bCreatedScaleGrid = false; + if(!m_pPrivate->ensureBaseLayers( + bCreatedMesh, bCreatedScalarBar, bCreatedScaleGrid)) { + delete pCandidateAdapter; + if(pError != nullptr) { + *pError = controllerText( + "Unable to create the result render layers."); + } + return false; + } + + bool bCommitted = m_pPrivate->m_pScaleGridLayer->setBounds( + dBounds[0], dBounds[1], dBounds[2], + dBounds[3], dBounds[4]) && + m_pPrivate->m_pContainer->setPrimaryViewBounds(dBounds) && + m_pPrivate->bindMeshAdapter( + pCandidateAdapter, dDisplayRange); + if(!bCommitted) { + const bool bRestored = m_pPrivate->restorePreviousScene(); + if(!bRestored) { + // 不允许 Release 构建留下“逻辑有快照、实际无输入”的半恢复状态。 + m_pPrivate->releaseActiveData(); + } + m_pPrivate->rollbackCreatedLayers( + bCreatedMesh, bCreatedScalarBar, bCreatedScaleGrid); + delete pCandidateAdapter; + if(pError != nullptr) { + *pError = controllerText( + "Unable to commit the result render data."); + } + return false; + } + + nmPebiResultSnapshotRenderAdapter* pPreviousAdapter = + m_pPrivate->m_pAdapter; + m_pPrivate->m_pAdapter = pCandidateAdapter; + m_pPrivate->m_pSnapshot = pSnapshot; + m_pPrivate->m_listAvailablePropertyIds = + pSnapshot->getAvailablePropertyIds(); + m_pPrivate->m_sCurrentPropertyId = sInitialProperty; + m_pPrivate->m_nCurrentFrameIndex = 0; + m_pPrivate->m_dBusinessRange[0] = dBusinessRange[0]; + m_pPrivate->m_dBusinessRange[1] = dBusinessRange[1]; + m_pPrivate->m_dDisplayRange[0] = dDisplayRange[0]; + m_pPrivate->m_dDisplayRange[1] = dDisplayRange[1]; + copyBounds(dBounds, m_pPrivate->m_dBounds); + m_pPrivate->m_bHasPropertyRange = true; + m_pPrivate->m_bHasBounds = true; + m_pPrivate->m_pScalarBarLayer->setLookupTable( + m_pPrivate->m_pMeshLayer->getLookupTable()); + m_pPrivate->applyLayerVisibility(true); + + // Mapper 已不再引用旧 Producer,此时才允许释放旧 Adapter。 + delete pPreviousAdapter; + + const bool bViewFitted = + m_pPrivate->m_pContainer->fitTopViewWithoutRender(); + const bool bCenterReset = + m_pPrivate->m_pContainer->resetRotationCenterWithoutRender(); + Q_ASSERT(bViewFitted && bCenterReset); + Q_UNUSED(bViewFitted); + Q_UNUSED(bCenterReset); + // showReadyState 是本提交路径唯一的 Render 入口。 + m_pPrivate->m_pContainer->showReadyState(); + return true; +} + +void nmPebiResultSceneController::clear() +{ + Q_ASSERT(isResultSceneGuiThread()); + if(isResultSceneGuiThread() && m_pPrivate != nullptr) { + m_pPrivate->releaseActiveData(); + } +} + +bool nmPebiResultSceneController::applyPropertyFrame( + const QString& sPropertyId, + int nFrameIndex, + QString* pError) +{ + Q_ASSERT(isResultSceneGuiThread()); + if(pError != nullptr) { + pError->clear(); + } + if(isResultSceneGuiThread() && m_pPrivate != nullptr) { + m_pPrivate->discardActiveDataIfLayersLost(); + } + if(!isResultSceneGuiThread() || m_pPrivate == nullptr || + m_pPrivate->m_pAdapter == nullptr || + m_pPrivate->m_pSnapshot.isNull() || + !m_pPrivate->hasOwnedBaseLayers() || + !m_pPrivate->m_pSnapshot->hasProperty(sPropertyId) || + nFrameIndex < 0 || + nFrameIndex >= m_pPrivate->m_pSnapshot->getPropertyFrameCount( + sPropertyId)) { + if(pError != nullptr) { + *pError = controllerText( + "The requested result property or frame is invalid."); + } + return false; + } + + nmPebiResultPropertyDescriptor oDescriptor; + if(!nmPebiResultPropertyCatalog::findDescriptor( + sPropertyId, oDescriptor) || + oDescriptor.m_eAssociation != + NM_PebiResultPropertyAssociation_CellData || + (oDescriptor.m_eTimeType == NM_PebiResultProperty_Static && + nFrameIndex != 0)) { + if(pError != nullptr) { + *pError = controllerText( + "The requested result property or frame is invalid."); + } + return false; + } + if(sPropertyId == m_pPrivate->m_sCurrentPropertyId && + nFrameIndex == m_pPrivate->m_nCurrentFrameIndex) { + return true; + } + + double dBusinessRange[2]; + double dDisplayRange[2]; + if(!getValidatedPropertyRanges( + m_pPrivate->m_pSnapshot, sPropertyId, + dBusinessRange, dDisplayRange)) { + if(pError != nullptr) { + *pError = controllerText( + "The result property range is invalid."); + } + return false; + } + + const QString sPreviousProperty = + m_pPrivate->m_sCurrentPropertyId; + const int nPreviousFrame = m_pPrivate->m_nCurrentFrameIndex; + const double dPreviousDisplayRange[2] = { + m_pPrivate->m_dDisplayRange[0], + m_pPrivate->m_dDisplayRange[1] + }; + bool bApplied = m_pPrivate->m_pAdapter->setPropertyFrame( + sPropertyId, nFrameIndex); + double dCurrentBounds[6]; + if(bApplied) { + bApplied = validateAdapterOutput( + m_pPrivate->m_pAdapter, + m_pPrivate->m_pSnapshot->getResultCellCount(), + dCurrentBounds) && + m_pPrivate->m_pMeshLayer->setScalarArray( + m_pPrivate->m_pAdapter->getCurrentScalarArrayName(), + nmVTKMeshLayer::ScalarAssociation_CellData, + dDisplayRange[0], dDisplayRange[1]); + } + if(!bApplied) { + const bool bAdapterRestored = + m_pPrivate->m_pAdapter->setPropertyFrame( + sPreviousProperty, nPreviousFrame); + const bool bMapperRestored = bAdapterRestored && + m_pPrivate->m_pMeshLayer->setScalarArray( + m_pPrivate->m_pAdapter->getCurrentScalarArrayName(), + nmVTKMeshLayer::ScalarAssociation_CellData, + dPreviousDisplayRange[0], dPreviousDisplayRange[1]); + if(!bMapperRestored) { + // 极端恢复失败时清空逻辑和 VTK 状态,保证查询结果与实际画面一致。 + m_pPrivate->releaseActiveData(); + } + if(pError != nullptr) { + *pError = controllerText( + "Unable to apply the result property or frame."); + } + return false; + } + + m_pPrivate->m_sCurrentPropertyId = sPropertyId; + m_pPrivate->m_nCurrentFrameIndex = nFrameIndex; + m_pPrivate->m_dBusinessRange[0] = dBusinessRange[0]; + m_pPrivate->m_dBusinessRange[1] = dBusinessRange[1]; + m_pPrivate->m_dDisplayRange[0] = dDisplayRange[0]; + m_pPrivate->m_dDisplayRange[1] = dDisplayRange[1]; + m_pPrivate->m_bHasPropertyRange = true; + m_pPrivate->m_pContainer->renderView(); + return true; +} + +void nmPebiResultSceneController::setMeshVisible(bool bVisible) +{ + Q_ASSERT(isResultSceneGuiThread()); + if(!isResultSceneGuiThread() || m_pPrivate == nullptr) { + return; + } + m_pPrivate->discardActiveDataIfLayersLost(); + if(m_pPrivate->m_bMeshVisible == bVisible) { + return; + } + m_pPrivate->m_bMeshVisible = bVisible; + const bool bHasData = hasSnapshot(); + m_pPrivate->applyLayerVisibility(bHasData); + if(bHasData) { + m_pPrivate->m_pContainer->renderView(); + } +} + +void nmPebiResultSceneController::setScalarBarVisible(bool bVisible) +{ + Q_ASSERT(isResultSceneGuiThread()); + if(!isResultSceneGuiThread() || m_pPrivate == nullptr) { + return; + } + m_pPrivate->discardActiveDataIfLayersLost(); + if(m_pPrivate->m_bScalarBarVisible == bVisible) { + return; + } + m_pPrivate->m_bScalarBarVisible = bVisible; + const bool bHasData = hasSnapshot(); + m_pPrivate->applyLayerVisibility(bHasData); + if(bHasData) { + m_pPrivate->m_pContainer->renderView(); + } +} + +void nmPebiResultSceneController::setScaleGridVisible(bool bVisible) +{ + Q_ASSERT(isResultSceneGuiThread()); + if(!isResultSceneGuiThread() || m_pPrivate == nullptr) { + return; + } + m_pPrivate->discardActiveDataIfLayersLost(); + if(m_pPrivate->m_bScaleGridVisible == bVisible) { + return; + } + m_pPrivate->m_bScaleGridVisible = bVisible; + const bool bHasData = hasSnapshot(); + m_pPrivate->applyLayerVisibility(bHasData); + if(bHasData) { + m_pPrivate->m_pContainer->renderView(); + } +} + +void nmPebiResultSceneController::setCellEdgesVisible(bool bVisible) +{ + Q_ASSERT(isResultSceneGuiThread()); + if(!isResultSceneGuiThread() || m_pPrivate == nullptr) { + return; + } + m_pPrivate->discardActiveDataIfLayersLost(); + if(m_pPrivate->m_bCellEdgesVisible == bVisible) { + return; + } + m_pPrivate->m_bCellEdgesVisible = bVisible; + const bool bHasData = hasSnapshot(); + m_pPrivate->applyLayerVisibility(bHasData); + if(bHasData) { + m_pPrivate->m_pContainer->renderView(); + } +} + +bool nmPebiResultSceneController::hasSnapshot() const +{ + return isResultSceneGuiThread() && + m_pPrivate != nullptr && + m_pPrivate->m_pAdapter != nullptr && + !m_pPrivate->m_pSnapshot.isNull() && + m_pPrivate->hasOwnedBaseLayers(); +} + +QString nmPebiResultSceneController::getCurrentSnapshotId() const +{ + return hasSnapshot() + ? m_pPrivate->m_pSnapshot->getSnapshotId() : QString(); +} + +QStringList nmPebiResultSceneController::getAvailablePropertyIds() const +{ + return hasSnapshot() + ? m_pPrivate->m_listAvailablePropertyIds : QStringList(); +} + +QString nmPebiResultSceneController::getCurrentPropertyId() const +{ + return hasSnapshot() + ? m_pPrivate->m_sCurrentPropertyId : QString(); +} + +int nmPebiResultSceneController::getCurrentFrameIndex() const +{ + return hasSnapshot() + ? m_pPrivate->m_nCurrentFrameIndex : -1; +} + +bool nmPebiResultSceneController::getCurrentPropertyRange( + double dRange[2]) const +{ + if(!hasSnapshot() || dRange == nullptr || + !m_pPrivate->m_bHasPropertyRange) { + return false; + } + dRange[0] = m_pPrivate->m_dBusinessRange[0]; + dRange[1] = m_pPrivate->m_dBusinessRange[1]; + return true; +} + +bool nmPebiResultSceneController::isMeshVisible() const +{ + return m_pPrivate != nullptr && m_pPrivate->m_bMeshVisible; +} + +bool nmPebiResultSceneController::isScalarBarVisible() const +{ + return m_pPrivate != nullptr && m_pPrivate->m_bScalarBarVisible; +} + +bool nmPebiResultSceneController::isScaleGridVisible() const +{ + return m_pPrivate != nullptr && m_pPrivate->m_bScaleGridVisible; +} + +bool nmPebiResultSceneController::areCellEdgesVisible() const +{ + return m_pPrivate != nullptr && m_pPrivate->m_bCellEdgesVisible; +}