From 36e52754183ced611965761dded278af7e05e652 Mon Sep 17 00:00:00 2001 From: arch1t3cht Date: Wed, 25 Jan 2023 23:24:11 +0100 Subject: [PATCH] visual tools: Add perspective tool --- .../buttons/visual_perspective.svg | 21 + .../buttons/visual_perspective_grid.svg | 21 + .../buttons/visual_perspective_lock_outer.svg | 29 + .../visual_perspective_orgmode_center.svg | 21 + .../visual_perspective_orgmode_keep.svg | 21 + .../visual_perspective_orgmode_nofax.svg | 21 + .../buttons/visual_perspective_plane.svg | 21 + src/bitmaps/button/visual_perspective_16.png | Bin 0 -> 629 bytes src/bitmaps/button/visual_perspective_24.png | Bin 0 -> 947 bytes src/bitmaps/button/visual_perspective_32.png | Bin 0 -> 1263 bytes src/bitmaps/button/visual_perspective_48.png | Bin 0 -> 1865 bytes src/bitmaps/button/visual_perspective_64.png | Bin 0 -> 2413 bytes .../button/visual_perspective_grid_16.png | Bin 0 -> 226 bytes .../button/visual_perspective_grid_24.png | Bin 0 -> 265 bytes .../button/visual_perspective_grid_32.png | Bin 0 -> 257 bytes .../button/visual_perspective_grid_48.png | Bin 0 -> 391 bytes .../button/visual_perspective_grid_64.png | Bin 0 -> 467 bytes .../visual_perspective_lock_outer_16.png | Bin 0 -> 543 bytes .../visual_perspective_lock_outer_24.png | Bin 0 -> 755 bytes .../visual_perspective_lock_outer_32.png | Bin 0 -> 987 bytes .../visual_perspective_lock_outer_48.png | Bin 0 -> 1422 bytes .../visual_perspective_lock_outer_64.png | Bin 0 -> 1840 bytes .../visual_perspective_orgmode_center_16.png | Bin 0 -> 456 bytes .../visual_perspective_orgmode_center_24.png | Bin 0 -> 634 bytes .../visual_perspective_orgmode_center_32.png | Bin 0 -> 824 bytes .../visual_perspective_orgmode_center_48.png | Bin 0 -> 1215 bytes .../visual_perspective_orgmode_center_64.png | Bin 0 -> 1600 bytes .../visual_perspective_orgmode_keep_16.png | Bin 0 -> 430 bytes .../visual_perspective_orgmode_keep_24.png | Bin 0 -> 629 bytes .../visual_perspective_orgmode_keep_32.png | Bin 0 -> 820 bytes .../visual_perspective_orgmode_keep_48.png | Bin 0 -> 1224 bytes .../visual_perspective_orgmode_keep_64.png | Bin 0 -> 1592 bytes .../visual_perspective_orgmode_nofax_16.png | Bin 0 -> 393 bytes .../visual_perspective_orgmode_nofax_24.png | Bin 0 -> 556 bytes .../visual_perspective_orgmode_nofax_32.png | Bin 0 -> 558 bytes .../visual_perspective_orgmode_nofax_48.png | Bin 0 -> 860 bytes .../visual_perspective_orgmode_nofax_64.png | Bin 0 -> 822 bytes .../button/visual_perspective_plane_16.png | Bin 0 -> 554 bytes .../button/visual_perspective_plane_24.png | Bin 0 -> 824 bytes .../button/visual_perspective_plane_32.png | Bin 0 -> 1065 bytes .../button/visual_perspective_plane_48.png | Bin 0 -> 1538 bytes .../button/visual_perspective_plane_64.png | Bin 0 -> 2041 bytes src/bitmaps/manifest.respack | 35 + src/command/vis_tool.cpp | 152 +++ src/libresrc/default_config.json | 6 + src/libresrc/default_hotkey.json | 9 +- src/libresrc/default_toolbar.json | 1 + src/libresrc/osx/default_config.json | 6 + src/libresrc/osx/default_hotkey.json | 9 +- src/meson.build | 2 + src/vector3d.cpp | 99 ++ src/vector3d.h | 83 ++ src/visual_tool.cpp | 1 + src/visual_tool_perspective.cpp | 894 ++++++++++++++++++ src/visual_tool_perspective.h | 135 +++ src/visual_tool_rotatexy.h | 1 + 56 files changed, 1582 insertions(+), 6 deletions(-) create mode 100644 docs/art-sources/buttons/visual_perspective.svg create mode 100644 docs/art-sources/buttons/visual_perspective_grid.svg create mode 100644 docs/art-sources/buttons/visual_perspective_lock_outer.svg create mode 100644 docs/art-sources/buttons/visual_perspective_orgmode_center.svg create mode 100644 docs/art-sources/buttons/visual_perspective_orgmode_keep.svg create mode 100644 docs/art-sources/buttons/visual_perspective_orgmode_nofax.svg create mode 100644 docs/art-sources/buttons/visual_perspective_plane.svg create mode 100644 src/bitmaps/button/visual_perspective_16.png create mode 100644 src/bitmaps/button/visual_perspective_24.png create mode 100644 src/bitmaps/button/visual_perspective_32.png create mode 100644 src/bitmaps/button/visual_perspective_48.png create mode 100644 src/bitmaps/button/visual_perspective_64.png create mode 100644 src/bitmaps/button/visual_perspective_grid_16.png create mode 100644 src/bitmaps/button/visual_perspective_grid_24.png create mode 100644 src/bitmaps/button/visual_perspective_grid_32.png create mode 100644 src/bitmaps/button/visual_perspective_grid_48.png create mode 100644 src/bitmaps/button/visual_perspective_grid_64.png create mode 100644 src/bitmaps/button/visual_perspective_lock_outer_16.png create mode 100644 src/bitmaps/button/visual_perspective_lock_outer_24.png create mode 100644 src/bitmaps/button/visual_perspective_lock_outer_32.png create mode 100644 src/bitmaps/button/visual_perspective_lock_outer_48.png create mode 100644 src/bitmaps/button/visual_perspective_lock_outer_64.png create mode 100644 src/bitmaps/button/visual_perspective_orgmode_center_16.png create mode 100644 src/bitmaps/button/visual_perspective_orgmode_center_24.png create mode 100644 src/bitmaps/button/visual_perspective_orgmode_center_32.png create mode 100644 src/bitmaps/button/visual_perspective_orgmode_center_48.png create mode 100644 src/bitmaps/button/visual_perspective_orgmode_center_64.png create mode 100644 src/bitmaps/button/visual_perspective_orgmode_keep_16.png create mode 100644 src/bitmaps/button/visual_perspective_orgmode_keep_24.png create mode 100644 src/bitmaps/button/visual_perspective_orgmode_keep_32.png create mode 100644 src/bitmaps/button/visual_perspective_orgmode_keep_48.png create mode 100644 src/bitmaps/button/visual_perspective_orgmode_keep_64.png create mode 100644 src/bitmaps/button/visual_perspective_orgmode_nofax_16.png create mode 100644 src/bitmaps/button/visual_perspective_orgmode_nofax_24.png create mode 100644 src/bitmaps/button/visual_perspective_orgmode_nofax_32.png create mode 100644 src/bitmaps/button/visual_perspective_orgmode_nofax_48.png create mode 100644 src/bitmaps/button/visual_perspective_orgmode_nofax_64.png create mode 100644 src/bitmaps/button/visual_perspective_plane_16.png create mode 100644 src/bitmaps/button/visual_perspective_plane_24.png create mode 100644 src/bitmaps/button/visual_perspective_plane_32.png create mode 100644 src/bitmaps/button/visual_perspective_plane_48.png create mode 100644 src/bitmaps/button/visual_perspective_plane_64.png create mode 100644 src/vector3d.cpp create mode 100644 src/vector3d.h create mode 100644 src/visual_tool_perspective.cpp create mode 100644 src/visual_tool_perspective.h diff --git a/docs/art-sources/buttons/visual_perspective.svg b/docs/art-sources/buttons/visual_perspective.svg new file mode 100644 index 000000000..55f08aeb6 --- /dev/null +++ b/docs/art-sources/buttons/visual_perspective.svg @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/docs/art-sources/buttons/visual_perspective_grid.svg b/docs/art-sources/buttons/visual_perspective_grid.svg new file mode 100644 index 000000000..a4354327a --- /dev/null +++ b/docs/art-sources/buttons/visual_perspective_grid.svg @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/docs/art-sources/buttons/visual_perspective_lock_outer.svg b/docs/art-sources/buttons/visual_perspective_lock_outer.svg new file mode 100644 index 000000000..a8d136425 --- /dev/null +++ b/docs/art-sources/buttons/visual_perspective_lock_outer.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + diff --git a/docs/art-sources/buttons/visual_perspective_orgmode_center.svg b/docs/art-sources/buttons/visual_perspective_orgmode_center.svg new file mode 100644 index 000000000..e833e3e90 --- /dev/null +++ b/docs/art-sources/buttons/visual_perspective_orgmode_center.svg @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/docs/art-sources/buttons/visual_perspective_orgmode_keep.svg b/docs/art-sources/buttons/visual_perspective_orgmode_keep.svg new file mode 100644 index 000000000..de11030f7 --- /dev/null +++ b/docs/art-sources/buttons/visual_perspective_orgmode_keep.svg @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/docs/art-sources/buttons/visual_perspective_orgmode_nofax.svg b/docs/art-sources/buttons/visual_perspective_orgmode_nofax.svg new file mode 100644 index 000000000..d593f87bb --- /dev/null +++ b/docs/art-sources/buttons/visual_perspective_orgmode_nofax.svg @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/docs/art-sources/buttons/visual_perspective_plane.svg b/docs/art-sources/buttons/visual_perspective_plane.svg new file mode 100644 index 000000000..55e9faf82 --- /dev/null +++ b/docs/art-sources/buttons/visual_perspective_plane.svg @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/src/bitmaps/button/visual_perspective_16.png b/src/bitmaps/button/visual_perspective_16.png new file mode 100644 index 0000000000000000000000000000000000000000..08d41a73e162609170ba8468c96e04d5915e4d30 GIT binary patch literal 629 zcmV-*0*d{KP)l>Q)#11ME)3=}5$t zf=HKL6gT2RTNs-~R;9WNv`J9}6$_2O7Lqp_jfKp6y2zwt%xwC=;T-P0=X=h%mn$N? zZ!sO?If z>;KXF$*l6Bzim4H(2iq|?C)4~L^6%u+PKXvfLh%ie7vUcbE(T`AI;malH9!rHtt;T z01Fy+zu&>IFnMOsN`V&@(!!fqN{DV_KR)8&fsPfusJWU2BfgI>Hm!6#D9R)FdE6->VW$&TXyy8wxtn*$QoWjvBuM8 zx6{~hWn4LH9vafD4yUH#{ObBkx-zaD$qOON8@8vhTd`%a@L0HPX``Z~B2-Yp`lesD z<$I6{Uu8vM6P`%EMlQ)QC*=4`lBZ-&AxEB)yl9x~=rViykY}qt)Bn}KAiLo$6dvX! P00000NkvXXu0mjf1Jx=- literal 0 HcmV?d00001 diff --git a/src/bitmaps/button/visual_perspective_24.png b/src/bitmaps/button/visual_perspective_24.png new file mode 100644 index 0000000000000000000000000000000000000000..fd973f9c9b8b8509c6502174a8091bdb89a17919 GIT binary patch literal 947 zcmV;k15EshP)uvA z=*o-kqPi?1DnwlaizJW&%WBtkS$frg z${y>n7l#BVM33P5Hj}gXB1HRhIiJ70-pMNkyo+Z;^z^Hnd{C)K3>pfFQlx}+=xN(> z44=u_RvuQH6HqxIZ^G|bAm@hm4Q&DRh3LVUD2YnAyXn#M!AkIPZbTB8-%W z)hZ8#{B@_~Uic+WwrsM7@5_cAyFC?y_eHDQeRTxH=g=HT zZ>$ZgY>LONF@e>A5Q&gn3fR8a{kR1OUK(~Lgb=2K6^k!2zroV;44q};NeiDlYSHou z^?I-@JF-VG@Y;x5TmCKsU`v+Cr?7pEmDf(14qYz%HFdcVEpFDvIC+Qa3~SF;$!r zt=#!hv#c|K%7_)X3qQOv>{pnWw06k_jqdL0p9VL!sYa2j72HYtJ&2xw5N69G7(7$Z z^Uf;gx&i8mq@Mia5R76&*672tfYcU&kj?_qi{7jCIQUmitub$oOzN`%glH@VT7AgF z>$@zIvj)euZSqsgxO^l|;JdDlJ4j$OyE3b10F9ij&9%1jTq2bNdgs*)q!eRbF-;AK ziV{Upk!*zQPP7AJv^9IE1}l4gC>ogAU}oN1Zykx&KNe&!PDh!X0SJaN<)a-oOreV3 zP!&`~)l63vR6{i0n#X8dQ0<@R#4I2xN!%u=CaLidRFk}Fy>9$!KKgd)`epuC@;CP8 VFM`q%r3nB4002ovPDHLkV1nRZ!qET# literal 0 HcmV?d00001 diff --git a/src/bitmaps/button/visual_perspective_32.png b/src/bitmaps/button/visual_perspective_32.png new file mode 100644 index 0000000000000000000000000000000000000000..582059c8f9ef0a7557891f960e0228e571faefc6 GIT binary patch literal 1263 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 DDy(oR literal 0 HcmV?d00001 diff --git a/src/bitmaps/button/visual_perspective_64.png b/src/bitmaps/button/visual_perspective_64.png new file mode 100644 index 0000000000000000000000000000000000000000..e76e6417651eb62b0c6f40cb797c9bb04539928d GIT binary patch literal 2413 zcmV-z36l1SP)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H12>MAx zK~#90?VJ0P6jv3;Kc{COGqWT+8y*@U!4OO!R3IiBK!qYA7+~1|T3i0+e~_|LQjri~ z7!)x^EB<6z_!n5KAz79@YFDL#r6vX}cb63tFqLQ^JOVR;UG_24*B@@rtNZpmdtE5} zR&`DHnS0N@{hf2pJ@+x9l;U=VHQgPUZrt_+Gz6{iz^mMo3DGVVXZCJpNWo3*V-jQx z*nfnrO87O%7WC5@LhW*0C0tQAZa(1nn+OX@2+Ic0R4N zV%-EJjv^T$VNo3L4@EHq@8DnM3f7+r3*YpJgqQ14xwC_- zz;B(4AHL50nT$h*unvfo>y;?h!W0v5pqF{TD!1IS>m$I8-J3WIoCSW_-A(9plt&b- zSBmw(8uF=k4ob1C65VwXkb!M@hqhC@pW>hO=q-h=E;7Ig=XxR0%?ye1j8g0`Dkzl^ zP}g)=!M6$SXlTfKuAF*A2DLUYGAd} z`WSD5M7#V4omLNuzJ_8+hM#_4jhE@t04@U1f$+^zm1-j3Cq3MO#X8Ry%4`0I+NBYA z6rkfHSe%555x5wJ3lZl&0^|9#XFiJgz($3(m@BQM6YX*a7z73y8~D|(zo-j^?TBJf zxx%r6hs(W7O$6Mq=mf$p@yGi*c{}bBp0`nw;bT;5$VMgaJcW%Lc+)UBGMpikD66eQZ6$In|P-FpsHxS+>`~#SDiRT?W%uGL>6%)|i zO$gZPmWv_z0T2;Euj~XyKqQ?XNkn-q;OR|_gLfu6(dZusDkfk!!Wv+XOFSBF<_%N% zeP3b;j=Ka!fTbXuw*rp^IH<}Es&Rqs9*N=&zwIp%?f)SKpBt^Bl`_b>f$|>;y@9Yq zN?Z{EqAhQmp|;WdAs+zej%X5sLUcMTVE=2Z0Pb~(C!67@Zza(IFo@x=5akGH$e1m_ ziD4d8M1Y+!m_PJ8i6u}Y9=|%GwEwrH^$aga$WMSfU?sS(eNq~fm z>;CUJk=6{Qhd)tpcYp~{7KQTx%O?&v$(eh@R;-L3nF>*o0H3*kY4dk7z&T@j87yc- z3#K}%^hjV@8}P&d-!(yJ-_N_lYx$eMgO#8cGyCV2E;t_2%i(pvmjHo5_s*3vj0C;} zq;+L?_Xgsdd9O^T9tORld-?NaO4bSGU7E{(Ckz^nXjX0kn4*6<^VfJ0)4U;%6sqRq zaH9y>4U*3&H5z3YFgW-!(cvjajq!j)qL*3dEu|uYEQ@7(HXC#8n-0#w%@lA)p}fK3 z3xy|Fj6qQJ)YoktXKt032eR9cr`+<4Dc5_L{-7Qtd_{r{zIBA+$WW(jS!&?eBP$bXW* z!axLZo)v98#vCT~YL5*%b1D5(HE^AWfRp;65W+^3C%L$^c#J>#X-ga<%qTHzVwOd# zQp8Zil+Y^Brj+w*2(6gzA(}ndl1&bj>(hfeZBbweMgq*ae`+|)FA_)fi=jK+>U_p1 zIHG13rZ;Y%Q9k$60pW7?Hb+3Bn8Kpn_khg#^tI@ygt%xK- z5=9b7x$ng$sVq{K%~|^-$GZytnUW*G2{_XM8%R3WRrFjzzmSl^*7xGv3nc9jDJFEC z_%@PNwbohum?NM$LjTwp=WteYs=#FK1(d_Sevu?XN}Lx^1utV+q^wDjjY9I-r?^&H z{nVmpGIZcI7Niy5MKKpe3b^XLP*U`BQiPBfHnWWwx*g(zx;00000NkvXXu0mjf7=&ZX literal 0 HcmV?d00001 diff --git a/src/bitmaps/button/visual_perspective_grid_16.png b/src/bitmaps/button/visual_perspective_grid_16.png new file mode 100644 index 0000000000000000000000000000000000000000..eaff0fa1847c59bb0c7e9a719d51c338e9e18ab5 GIT binary patch literal 226 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#S9G08$g&*{RsbBprB-l zYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&Kt=hUE{-7Bnr3Lat*V14*r zBIF3`1w{o>r2?S>Az>4a1bP0l+XkK1g=lx literal 0 HcmV?d00001 diff --git a/src/bitmaps/button/visual_perspective_grid_24.png b/src/bitmaps/button/visual_perspective_grid_24.png new file mode 100644 index 0000000000000000000000000000000000000000..73fc7b430adf59bc16a880c41cba22ceef793bc5 GIT binary patch literal 265 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjoCO|{#S9FrogmDZn=$tnP*AeO zHKHUqKdq!Zu_%?Hyu4g5GcUV1Ik6yBFTW^#_B$IXprUD>E{-7{oo}ZeGkYV%o z>x^L~--Bl#oy?XL*ut!6sFVdQ I&MBb@0E0_k*#H0l literal 0 HcmV?d00001 diff --git a/src/bitmaps/button/visual_perspective_grid_32.png b/src/bitmaps/button/visual_perspective_grid_32.png new file mode 100644 index 0000000000000000000000000000000000000000..54f5d71fc7b105655e0325458dc4991347d94630 GIT binary patch literal 257 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz&H|6fVg?5GL=a}2dU(e+prB-l zYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&Kt+9?E{-7)t#79s_55-slY5%T&Lt<-R)jhj zaI}T((c0jyb0JniB`_$Z^EQ7Acf*6t7Ju0L6Ly`il1*mLu3T2ZY?BrnU0`v5H{t-( yJI3z~tU3?m3iu-y>^@M*&DNu98L}^-zlO2rYVo^Dn{yzqF?hQAxvXJ#^I+)v` z&~h_`lN15AKP z^OoiVtt!+^SCDagHu=LH8A|{TJTc<);+MP3AK1=c{P$5{F>`sn z=Z*jWa0+b3*G!*RerFYuUanMprB-l zYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&3=E73o-U3d6?5Lsw)Hv^AkunX zv^+60Th(vr(>I6aFSU@I{^*5XP?l*zYW7R3b*+_(4@5i#?2O*mTvaZpXz$-MA;u$R zS=O@_J%?f|a|ywVF|WcV&Entt>_~LyIq@{^8*wl7g3sTzQh7UVivAfpCe69>QTnR! zEB;9)FqSY%aWCM$5Wx`JaE$2%lSyFQ!qdA;Th1M0G2RlE^;bGfdS%(EV=BizuPSE1L=ZGft;VVtzp7GzO~aQz2971KW|xm?ceqB j>FL}SVyo)e{xAs5x1Qqr;H4liLKr+<{an^LB{Ts5T`|IK literal 0 HcmV?d00001 diff --git a/src/bitmaps/button/visual_perspective_lock_outer_16.png b/src/bitmaps/button/visual_perspective_lock_outer_16.png new file mode 100644 index 0000000000000000000000000000000000000000..17ba65ce43e68593f489adc25d5e83927f9baed2 GIT binary patch literal 543 zcmV+)0^t3LP)X=5u{4I|JTudpcQEc zz?w#;5dGbaxAJ&i%DJ@!bOUcsdLaD*?g3cC@O4Bl=lAPCFDMiqI?XaL8f~K44Wz9i zV=cgnvY8{WPe@ln%B~l*4z3j{D$0<2RX-@;>f6;hO*e002ovPDHLkV1jw)?t%aS literal 0 HcmV?d00001 diff --git a/src/bitmaps/button/visual_perspective_lock_outer_24.png b/src/bitmaps/button/visual_perspective_lock_outer_24.png new file mode 100644 index 0000000000000000000000000000000000000000..d2c700a91ef44c103745006bdd25c50e3cd92151 GIT binary patch literal 755 zcmV+#MKMU=2d<`IZ{ciw4f z;i&XXHRFt8adVCx9IxgFCa^{3iCUZXn_SU-9G}l(-Gx2UCK1CT2XUYOrbimI<_%n0 z*dDY>bJD=ju{!@l14HNLtLrpDSEKuC7uMmMI`A}BMeHav)h=&vTlOI0A$%qCeuxKm z4Uk)j1LQ}-@yh7&GzQ5R6lM-*>HSM09>o_j`)lorZPvd7-`Dwry?5RHT2F7^jit+$ zZ7&px+hBTnD*nED_20?KiMOu*Q@(-^a1JX)^vk^1iC}Xzd3L5X53kSd&pp4SO|JLe z*;oFxbUG?iWm&{otVKtHoweXi+LO3kb6N>9#~0C@%!Xsyc#0xMM66yELBt9XFSimz zti&srUK9b#@w$jRXq0FhPRRVdx#G&i<-(~SzHQsBT)x&Ua|u%__vWYi;3<^}8ZG_! z$8YDyK0Y+s_EKi%l51=2t_*&-m+6pAAl8wm|NnaJ~NnT3c zOx{kGRPo{D=j1~2ZWW8U@v=KOwRlBlP6b&U?>YPK{x0Nepi0KX!YrX&MNwS90@P!dQ`6s)b;H{+|l0@5bT z+JG)#4bUH}&UWA#;0NF&(v7LFKz4QB6%4xS1cKeQ`qo1JnBc5n(Ij@hV3lB(;DwmJ zOK?Qc^?zHi8F(5<9XjjWx(w6fbZ#l=&qF;xPr@W{Eo%aPtni<7515?-8j)5$pl*M2 z9$xM%#Pc!r33B7F>5nf)5*(ib89hAhG<2(hZ!59fVHf-w()Yl1U_Ilxk5ZV< z0C~)2RM2Erz|4THz&A+4Wp(FUPa*F-FhI#)qWx#|IW5 z6_O#cD-caecRaXr+vM=C)!#h?puX-tpciPSy|6OD6Ql*7Jb~)+M0D8cg#MB{+iNAN zo`Bg}%sxrQ#GJ@Gv2dhYkuS|Dd^<5aRH3dq17^p7rAQrRRmYZh{9Nw6FbHN#%=CSK z0JCzh6$UL~s20!jhJb;v^zRxKz8rRZ2Z6VN=J-95z6Z=^0qcQ}6TpgwhNk(;pMB1| zd-o2FGn?kd(J%}d9l1#`9^`w^_2ls$dK8$Lp9?@Eu!#Gus=A0aIOB?+->lZw@Wod) zXUa46_EOXJyRGw`YFPJq`K}e zqRIr!UdL?Xv_vy&#%u>><;r^rgBe7V|LU|P0IMAT>huI)9gqi(PE!Jx-MMT+ksQL8 zfl;D|XR3_-Giql~{gfHy`n9X+fOG}3L11aM1TF#}B8{aIm|eM5Ec)La{wyix&0_E&M1n9Ttm0os5Oq{}gEC(-t616qK6NM|Y&Fna^|8+aHfBAt$5 zyMg6Et0VaU>7>JU@ZgO%fY4=HKpMz$oGPjN5Z3)JxCauW6r)JZKk@(o002ov JPDHLkV1fe9(aHb- literal 0 HcmV?d00001 diff --git a/src/bitmaps/button/visual_perspective_lock_outer_48.png b/src/bitmaps/button/visual_perspective_lock_outer_48.png new file mode 100644 index 0000000000000000000000000000000000000000..9b50518d045ece53075cbb4c7888039b66a0fcd6 GIT binary patch literal 1422 zcmV;91#$X`P)!q!?lZzkngWi1>nHMTtd8p)r6+@IisJv{o$b-Q$Oy%iZqX-R`~HTT=eX zW_I?RIp=?NW@paKimEb_5xdv~qGH4c5RcSaWIiwh_y*X|MHr6~$*0Ikpw!gbfiHm_ zz!p?{BeM$wRn7i_)OEcQ$!-Q$v>r%8WD;-}a3?SwSb^%tE}c5yYV2OxhiW3a z)7x?15mg+m_0?Xf6!vn8`sVW&b>7H!KE>J>IC z>{XcRlIJO`QCO`|J@f(C4o;f4SD%BPn&s%7lWKs1SnS0LwL`3NC-v~@FkGc>xAhH(t$-(j^DSvcgfUoaN*&jqG(_H8nunuTP z^;b`gs`>?3)@)Pj!IHW9d*eX>nf>@E&YW)o<^YAn5=(%!KE750+eiiCiOhOHHG%3= zs5bcOB&@}b2Ja-v1L(f>eTcqwK`e52fUjOuccB{C14Exq3%923Wm}p&k=Nv?O^fPo z=mStKzi^))+n**sUVaR)Os!O8btDeq8;guaB=DP&>z5ZGGS*akwQWPUTSs-31@v6- zKec^KfKNf>Hblnzse}taqzw2Q*b}JN+f(AGoe9uP1@=$ZxB$Hef!)CSehOh{p(WVU zqUW?HRdo$mx|q1+lUx@?N~E&xh_^Yt0>r+J#lM%zx^sxUtVbK&MDt8JFKPhyn=<{Q z1W=wpq!{O-u3CV-Zg@bPZOe`R!=%6IUrbqo$Tg_`;kGn*03z1{H<_B>>MHBQkf2WV;D4XWZHZUfE(`PeIR76HIQL+}*#c=SbO^%Tk~Cx4Q|5aK!}P!!;Ag-$Bg`?L1wO~)?L(LVR6Bt8 zhLln!Q0*ch^BwUgbC+-cBE^U-Mr8Cb^6`hr6hsQ#kAr&PZQw&-Mb1ctg~$WIL7>qs z@(w^`3~;ZhoyZxn@JJ?e^W6f!9;gMz0DZtGx#5%%k}1VRM6PrS`~p~H%AIB!z1V04 zl8b1KQ;@z6*g1c#srk;oUfA=+rxcE#Fu0-vr~XPyZ_=T94w1va4cKYL*GZAzLe`0W z8sMFMrsF?=wgW98)%|S*U>|k>12#A50jTyPawCw3YTAy<6T(uI1>tylK`r9(4+c?s z2Kz*C%K+EIa=e~HMEggU3y4$zvw$MURRFx}bruyp0DE;T!rpRrqT23~$ARa8Y8R<~ z;2~679lE1{79)3~6nG>!0Ff85r`CgpP!Z6K>MobwTwt>?dYMD=FCx{ht=}!cY7asH zGfAC+CO0t6z|GhbNrx#<18;g6NChA=6IkV;(*YE_p3MTT^U&=E+K_B0EswQ0`+kot&4CK_iY4kw+!c2r*-*h=EqKlLmZsnbOH-<1VM{48_{?*~Q* cMtlJO0}xAlr;j$`GXMYp07*qoM6N<$f@~6uHUIzs literal 0 HcmV?d00001 diff --git a/src/bitmaps/button/visual_perspective_lock_outer_64.png b/src/bitmaps/button/visual_perspective_lock_outer_64.png new file mode 100644 index 0000000000000000000000000000000000000000..6caf315620bcb5afb60965d8c7c9598423aa4769 GIT binary patch literal 1840 zcmV-02haG4P)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H12E9o{ zK~#90?V4F^l|>ZCf9KM&YAIU`5NwNpsDNM<5El^KkSG<6MiYfLM$njGG(7nLB)%XI zlDGsUC_LDx5d>61Fd$*qAiKe!&;lX?kxg3J`W+wUEBCv%-`e}#dmHUPncSJVXXcza zbG|tc9|34JEhV45p zH~6$8e>|o>lTba6>SZ5_21mXpFd=N$nY%%euD}?(q?kQG_~vDmV=(Tr)NX$mOE>D_ z#Zfj(4+HN5}< z2$4l36)-rA3=q#waFQecRhR}+R#l1UYW!zvBxl(f~BKVE3FURX!Y2FdPLTwR3;L4W<##r#RSW$ z1|ZViQFDfxzS}+NRd=M6t?;fb3Ua3atD#>?S=gaE)AFQG$bmHa-q{cX7r-*B3< z5pw$Rm7Y|6tIDyg8F=Ye=>znMz_&e0xt%YktadpjUYL$wfJ;C-pahX(RL^>K+MG3@ z!jbnr{8-w_^S?Ntxc6_i*8A;}0E7IxiDm&Ji>axXB{&JQ8^$u>dMPr!NfT^SIiNir!YphL&Z4Wghh>^gXD4S;~yK-5p*K}SB+ z**{Nk22lq15k^(OkM{3DV0FZ{xx@ybaVNRTk1*;dFdbM3)Py~5<^`%35g810L)AA( z8%+Tw=&&b{t|h>%Pk|BY))ylA79ihfRX`{zkskK-_J0n+3J`f5vzQ!3#Mh^`IEaiu zqy~{Kh+OYR62=0iW5U=)Kvv>%Swtn~%I$7a4{_JvuKc|gu zBM7p9i6r+5Tf^S_wIBpumjx-ndH7kDu{gPhQvtWT5qK-WEE?k+H7)t@>`s zM4CjF{kFo7ki7TXh3cW^N^xM{Za$hjySdG;;oRxd(Hf{KBKv@Q?V7>BF(-+i0-W)- z>S&ySgfw88cvyYi1?ubkFLxtDQeK7lVJAriM6!Stn9If@V16Xw0P3#nq(i|@&|#6= z2IE(*v(&HV&XOW|BTfwbr9uHSnBI&+Acrz>{rk3NTMy zhmf2JXKDv5LDfB)B(ehY+RO{PP?yD2q&-lNnQ1TlgsSHmrs*djG9S1f_!RR1u^2Ne z9f5_YdYuPWVczR`VIt7-7La=H z=Z2H0-r(MoTmX;T8Uyc{5P2PVmE<5A(>cujVKu5pUE5}ebPMPbKwsM+8e zAvqhTaPh&{pgIqj7r;9!NVk9l=C#+YB!xdiau91#J(qhivxk=B1@eT}plH4Y$TwOQ ekZ%FBuc+?FB$0000h_Wr~c2Wakf1Ha0f0m$I=@O0qC1v9_8WMQJva zY?RcjW@BMvB{fZ{SvVHoH|9Iv`Hms4UcGbAySH;r*Cg?u6A7)FL8VgiUddBd13W47S%3TDTc%{zR7*$wdI yS@8D#Ot9}JPYTuqGY*dkMg{ROC5@Z-$MXd@lte8ZpH8j-0000gLfH))ON&wDwJcbf zh4NC2g&G!PG2X(M?i`D`mwV0cUW3t`dKS-l&N<)bobNd&GSmN*mZq-}b40BEe*kPl zuZVsThyDg2ep{l5>DYoPGOvp$;VK^EhRi!n{WG)pFhAMU;B9Bpo%AIg72UFA)vvbA z08S^}4d2Jw?@or2>Ei|HOLqJLKqQ07hFX0w$)k1e$IWrGIZEb94Ah01oH>a2UHdkc zh`F`bDFp`0aS!da>NA}&<8!OQ(uffuq0H0_yN*4~3l-lrxP^s391F-goLns;va3^1 z?>yxszn0{wbv_(lYkrOI%w!~4*o@?@xKfn;aLK{Di+nBz_ZRpU>Ef|R`IFSlPk4!r zW*IC_N{!mFt5eVYHsvwDuOHBs#u;Sx%PbdVP|f0YJHn;9-~aRwqiVXUOS zL{D(!AA_sZ{~SIsk*gwNvC#pWjR!KnQ~*=5B-xZ~PUci}vy%NuS8_I)R?+WCo+fXS z^EI3FRq{4@o@}ggQe73hu?O$550^0u>+un9P;I?yMu+JgUi|nah21wv+jIiuJE8;# UNh6Z^<^TWy07*qoM6N<$f)@29GXMYp literal 0 HcmV?d00001 diff --git a/src/bitmaps/button/visual_perspective_orgmode_center_32.png b/src/bitmaps/button/visual_perspective_orgmode_center_32.png new file mode 100644 index 0000000000000000000000000000000000000000..6b10a3131090cfeba18f945c9fe0685019767fa9 GIT binary patch literal 824 zcmV-81IPS{P)w#0 zK~z|U?bgj}TxApn@ZZU#jiNS5wGebs6f7csK}&Jb27{HV8@p4)jbOo2Tv%P$4^m47 z{{wejxpAddDN4mI3VvZytr7@b2rAKniA_UAl$kj$=EhvcnQU!#&S?-uujX z&&L~?sh=3=>(?>Z^~#9X@drM_7c#5sq98N7dpMm`lS~$q@veP4Jl#hi^9d0nn7}Ul zSc=Ym?7+u3Cv$%NBhWcIFC_~_bZW`rZhgDi{MKYT+4e8%D#^~|m1K9xK9RheOsrPG zq2iz2vC8O-C-ceK)e86|nO@@&ol1Ug#dYojjih@J?{uA1tuckoA~y7vghcXOGP=eH zRFbW&xXAp!6}LgeIG&dId@K6o_ShX9iWJRUEnJ?QZt{ybh#fMY?M?tM(uTFWtjR3g28vz&b8b0kL zVDiiSppq_}A5&Y}$X8ort>Q?wF0C0H!RKY;n#=IS_INZnbLW!yZj+f~7u?rIKufH- z43AIehZVlQG%TtEj`s5L)&e4)6!Ca3n^e<_1L>_cXJ6j?CJ_fjtW;l9%iK`R^;2tV zz3p4_!^LFX)ovI)gv~hJO9XnrZ1H>lvY}bPK3vndc&YoB4p1}{e0gEy}D{+VM0EUZA zj>$Y%a0l>4NzPBWt>6ML;1(=m9z$jAgT=zz@n*@sP&h4PFUB;^?pOR);@(3|BYCgl z(%($ueDBf$D>)GH80v-6?`2-@GsV literal 0 HcmV?d00001 diff --git a/src/bitmaps/button/visual_perspective_orgmode_center_48.png b/src/bitmaps/button/visual_perspective_orgmode_center_48.png new file mode 100644 index 0000000000000000000000000000000000000000..9319343b3e41bdcc6f464cd46ef9b799d9842a07 GIT binary patch literal 1215 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 YA2BicvxlS>TL1t607*qoM6N<$f^CDZ3jhEB literal 0 HcmV?d00001 diff --git a/src/bitmaps/button/visual_perspective_orgmode_keep_24.png b/src/bitmaps/button/visual_perspective_orgmode_keep_24.png new file mode 100644 index 0000000000000000000000000000000000000000..bc6bfa2bfd56a39380ccdff454b83cf952bfd708 GIT binary patch literal 629 zcmV-*0*d{KP)*?s0Qih(pWS!)X?hCu0u%>wGE~1)5NS{dg(Ed{2X~FKVwp(Cok&zW1E(eeUx-_uR-#D;ZVuRwL>}>{~ei zPU5+UArY6C0uT#I6tNb^Q7!Y4h*ju9A0EqmvNV8*dNj-Ion4x!j&twUYh5$p;L|PI z`ZgKbKdSN6u(#`f<%`7v+{R{^=VzC09F6_Oq8HnK7>&~JBkOEz?f1L6pibt`a&u-@ zP6m^+bJcIP=y|nKG0FEeva`iNkJ~MTJ${JE<0Pio|1cZ&c$G%>sw~gThCaPj+`kmDXyLj-)fWldLJNwMc{ zK~z|U#nww`TxApo@ZUG)(c07`wLVB(C`b{dC~3edr6?jrHwFYBAh;4Kr6A}g5Rg7V zs<>0Jg1B%iYAapU)};v5g(6f1MWZ!f5)z}0=`=Ym?k%~UcJ9oS8Rid{3+M6O|2^k? z=bRJ~x`|3xzmA@!EmL|8D>#X-L|kkOgNSJE;aG4nh+rYu-L!AX(|iOXo{-WI9>y+A z7ozh5#&HU#Ma;H80_D+pIhf0%vl>h`>zf+hf|N$^8J-pKV?DFO<7r~^1%GVA+h5xC-A`4oXYz^R&+b@Nh6l8o{8Bt-a0jE zWi4g0&qq}&;iKD^oWvF>^|nTUMZAc&8xhEHWT{VlKb-yj^njk#ii3C$+eB1r9U2#5 zi#RZz-WXo7f1s*zZj(=^zK*BL>^sqlk1Y0!-`D@X93^*qcku4D^o4^vOdTF`wC22h z!3VXrWdx1~zqF-MW#_#fEC)SB`PEa9(f}UC$8Ai_x7dJfMPa=f*pnMKynZbbfV(e~ z)(O|0!J(ki#uQu^Oa`~LHVZn8u5UzF5=iN3DLvkiucfqEN_(ZWK7(rV)SJ)s_uH2H z%g=u#-x+q*yM_;83yyUp0-Vj?_g)gz5}3e0vckkWJb4V$na;>&_=68B*+KV(Y8 z>0GxL?-b~q!)UHc*pIDPz%2TU+ot(lV_3_X{wiXo%&}eQdJ%Sg7W@z6N53W3W1{~60000xpPHTsbo0iHXtb~-hp(Y-Xa@-b--7^5y~(hC6P{%KY^9DbQ1UiI0WoR zbt18KVW6t@FW|7X^7kiJOJDev;i-o`l*NKX`l%+N<*klC0Cqu z3We4p86fMb$pT*jp93GDIvHDbsfJqNC50b+rrYyL^TcfJh4X4YT_^i|YGPx_93zY0j{oG*y1>ZKt^D!4te&RRGM+wyN0n z948LkjWJ$<-l(1B99z}lBMScQBMp#u_abSz3L#UpJ+F6%yNG3S9 z>H^E|`*m*Hw<};QgC4t*?LZ%}6tk-ZIuI?D5D{RLEggu|*P^pu|5oAc-)mv%B($70 z&+32R=Q^fP4(>dn!@#KR(pF4f)Lf_qSYzK~IRL;EdmI?=Yhq?@4>ZDrgX1W>Y*oLd z`J%ES?jR|Jy$Y`+-B!Cgc)hCw{i6oGm_m22Bj44|$1Yvf3il|ai^++$22Awa5vflQ zrr6`8*wNR-va2s56Aq43nECo(a;d|Y5>vKxx^q}_1=xGE9{L+82z>JDclv!)IpsYM zRPmN5=O-OR3UtR25k8QKTmj@!ExxECqWd;^4!E{+NMG~ODVVr3g6dgc0TIG~PGPWQ zxs%cRxXqV(f_TDCp(lW=faZALIW|(3t`A}e>%gOy9!z9Tbc|TqfXHe;hD$pjat(01 zEyWLB@h}GbYu|k%a+nc#1Q6g9CO;`1%t2?&mTvW93FAPEr3YA`V_-xKTI!pAhuI10 z6}Bm4O1cBh>aSDStWdKs2b%vGD*n&hW$b{+azq}pDMa}pax)^;^XiK%p8YnA?<~vW zoomY$Ys-h>QVEe-;2~T3W$`|1Xw+G{!P^itg*F1Uz$Eb5;@A`k(Zt&Ryvr ziOnKTp*p_!c7=pUBk(Zh@?^%Yiccky-H|N@M6L%qfQ;*@2Hr#UM=yJF2TXL#V2+$Q zR8M-^)4tH-E3@Fc3EF5V@;uxNpF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H11;t53 zK~#90?V5jRRb?2*KhHL&*qkmif6c5+D_NE;e^i!6WmKrcf+(m6q9_8R{w$av5+#9u zg&_n54I;G^lJGZ{v&7c?5lK{=Ej6ai&2mhg_vw%K?B27xbKZN;J?GwYm;1uuKJPux z`@GNleV_CG7FFd+MoHWb#El^W;&vczL{ET7B_ef*Opn}l9w3o-f+Bl?`44gznWx-Z&7b4Nbk%>~}gLzy|FEOLvg`++({ z%7FR6!_G6uDX-BtugGISC9nt8zasa|tH^>)ghVQVDX9MB^L{z7(Mc(AgjE0W4XWLF z@k>>U(0J=~6wTgT+-BSTOuD3rdyg3E{+!YL*_WqBC88;R$ZMxsyc`yZIh7+@8~ z3HZ=Stw#0jTurT-BeN4qBQPRp-lfN3_8CqAHK=xElP9WS@JW|^DvTjba4k7h9^l+C z044&DXPYPdFeq|0u*gX%W)6@uw2AH1auld3QCM;u$`k(j+p3|f9Lki-wPz@G{+^X> zc~L~bQec=Pe~0RyVT{$QCQw!ZBm}z4VCGrxXVfKAd4TcVY^}JIG;>?F-{GpGB%sC!bZTYq&loasQwz#UJ{w2z}@;JcpudSu+90eofH{Xge9OE z_zE})ycN<`KiHxmz4hyx^rRy{4?GLZL$xQHa*Agr=m8Otm9;bjvqtv9iX&!3_+&tj~dnx$O1Wl*WPU`4u8mhlX>0RC-B0vYww<%JM>It`sEdm}PRr%&Y zS9vfuz??G$2LqPIHUfHGazj);#eg4O^5pcFwogDX2aT1$I%lhC@WEW#-UxI7b?Nt& zhgO4eI}kUbzbhOE%!oA()7y`Be#c;Tg*V%-=@-u@ATkxW26Nf6uL7enjr19+tv>l0 zfTt~%_n`WKZ?TaPV6M8(BGnS^XE^XNs)v2@ZpPd;^FuSLvpw>S0JrwSv4{QY%TO3&i3_Pqf#64=s2WB1O@vrtE@*eOaa1L`; z(dGPh0;hrXHpBfRp#Qm_7ur!B?`=u`1+KE>^=}E0*MV0_Rif|d!pue+P;K+f8xrXe zP?bTy)$;CTOeM}^oIgvdHBJLx`sP$!3#`phc3zMk0VSB*UNcEKf0k4wcB0x^aO?T+2j8mkS+EKW*y8r z@ZW>0h9TO%K2`x*zP`^H;JFs#JvanF8Z=Z=moRK}YijVKY6E;>*264!r^0!LS&CWL nkuo)SNhrY_Ru7&n`UdNJK8wj01>qDJW0q<|{_bkvC!e z5)=^kpjKw!iwG`TniOV_0g&Ew6TB*g0B*-lJVnPo!`V&s3+c6I^$57->*;M}Rs)^` uBfvl4B~s5}S;dX^Hg?XZs3KJUSMeXv%PG?e3s`di0000OqUVsLP4iNT+LsD`A%xt^%YEN{-`%~4s?tn2P4)g42&8JBL~_7X zz;cM{mN(W1>;moHSP7Uz_2HXyt9Fu)z%Vc!G)SVGP~->z)rn{VZWsxMC@z*qWeS*# z^sAgL6}5m!*8BdWKb8u;>xbvt1W?9+`GFV8qEuFO(?cslH&6&Vu1DZE*mPiF@R^RG zXV-9>hU;Ebg|&K<8ny}ZMSc2!?UQ~=AL81M`zbrhUjAG!0as#Q<~juSz3Zn~e{Dmr zGVbbc)1=-VRs!SG*v^9G5!uL;$fe6T_gx$;Ug%8Ko16lmI_{fn)PePvcJ$6%eI#lL zh-53N@;qKrr^GfJYg|~T+8!5~X=f()&ctStnLs|c%$a)+Gw0mjbMF;ZrI#5<+kkfI zJCHVm#$1sRpb*M+P^}ttbHI6mm2m)UqPlO;p8|$Mx!b7zGU%QHCL(oIHJBi;P*qSv zDjW0{ExL;a-Lgfus*n%qmK2;w9o3%dLPg+QI8%(%Xtui0Rp70ga{<-Q@eX7Ok(0o- z`&K}8$6st4#AN_fpCxI_lN&Pj+u;^1blpK%yI;{QRCf@$?%K0s&*Qjo_ zYq-bmF82@~x1#$;cmdo6K(%7iGqu35MQ$Hq1$YF=wHo9My|;1}oE*pjE3JI8tFzEt zMLz&b0H+RA7wJ(wfHh>LR4q$Si27Gf7aplAbw}Q3yTfn>ym{!H)SpaUY6_AO&MusT!4}-NIOKP!@HzU=D z;L9j*jM|6v_X7)ng0b&=I2IYOIUrI(q?Ck(QMj$}9AWv&MP3?}8j-*(Fl&kmnuEER z;3;dET3{R)a?^Ns&G~=Q9s_ODcc9~lE)*VpR$o?3Jp%)xDu*g94(PFov(m1NX8=oA&6iVcQkM7&0PVEVj!qkN# zcR*RA7sfy&eFwUNNcr%%GhAdC#FI2mBmjy@9xWb=7<){i#1pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10>eo} zK~#90?V7!A6G0Gwzcp4wkw^#$6cJLTLumlf0I$FkVAJp*Hi{4hqD9c8AWlV|0FbCi z0q6>p79oN_bcg~gg6M|g&W@ejp6C3N-8=6mjdb?yc<1JJW^UGJM1*;!=Gp+)1P8b_ zz%{`Et_^Ta-Fn6t1Kb5xEF-=H+hmmyzjW>60YoHQpeZ0Dq*)T(?C~OV69Q|t6ZQ~* z4j{m~<@_G7?Xm7{;Fe|F5!MhWyFhy&1lsc$0-r6%Lujf1x$?yyiN4zhH{K}T4fBlOGc-DF^Ka0-iodW>c5ifyH0PmLJuf@u@m->eX#5@^iE-xJ*Ng;m- z$qM**;pCWai|}Qs|MBUu3XoJWV;ZR#+gx5c07kC@#occJTd6c`^W-@|^&l}T@>apf zyPu${VtEee0L@f7DyyfAZ52!-_IubJkUT7RQgO0%oV5y+cVCUSIiOF(4oL^d?lQbN z+Un?1N+54fqR>}hNLY% zfw%4O6i`($&0k}X_aISn><&m`#>qawX)^a!AUMFa0j{aC11i=I7bmpCp&eFGx#LOO8Ze&>Go<2&ablf?f-DzR#|jM-z% zPX1FtQar&IR0LVUsNkjGZW1@?@w}j`rPd)Rx${H8Saj#NSoeWpq>cRW{juHpZm{or zA0({=Vsn=^ewc1Shc6J4`G}0ar zb5KTlXT`~Map>BqQn`{Tl`93Hu>E_B#iz5kfEC~}@TU0}%sk);0QKC++4C2=k7~EI zZx(<8unf@Cd-~?!xuK4u0x2(r7o>9zz5qI#Tadm)4*Vc$nStHD}*|M@Z8E=}hKKEnJTYn2~pfZeS9r8aWsRPDI4b@aNLv!d#;TlYB4O0{FAH z|IJahf!jbeT9iIPA#p4JmT*JT;SlqH=j;t&b_cTwho>;BVm9vZbIe`??;G=I?E5pp soKqeV+!I`Kcu;UrkWF$D-^63a0Uh7{BsEaEU;qFB07*qoM6N<$g2YJsCIA2c literal 0 HcmV?d00001 diff --git a/src/bitmaps/button/visual_perspective_plane_24.png b/src/bitmaps/button/visual_perspective_plane_24.png new file mode 100644 index 0000000000000000000000000000000000000000..cfb2dc37b121e223fbdf40ce917e1c3ab25877a5 GIT binary patch literal 824 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 literal 0 HcmV?d00001 diff --git a/src/bitmaps/button/visual_perspective_plane_64.png b/src/bitmaps/button/visual_perspective_plane_64.png new file mode 100644 index 0000000000000000000000000000000000000000..83d6a341cddfa266c59af18ecf88bf91f7f16cac GIT binary patch literal 2041 zcmVpF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H12Zl*R zK~#90?VDX}Tvrjte{*+T+q<^o*mas@9ovcXRoW)B4RJ!nN5z)_(U<~4R3cR>)ChzW zL=i7M01_|Imx`xK5f6Q6B!mJ@q_(J~9}1yGh>9k5TLf*A64y>_lGuSJCfeQM;oRN5 zUf+A}ew_6>O8?T8&)qwB&YZb(X6DQ}7FFd&hSzKXnvD+>&};#k4fhHV=|W@_kxkBd zHvr>v1xA670yl+F zeG1hVop$R)0V1uyqrghw5mcw^yjUH(8#a4)i~%&{HXIS7Gq0f%cRmo+YSWM)jJ7A{S_{0lru%>-rTS(gS?b zzf(*Ou+;c0@ND3ITct0$uz>r37Qg*xR3{wptPZNDfL{fExueooT`1uGK>K$N_|=M& zfp*0fP+t@f=>TScHvgARsJ^|3Y|}%1Wc3lyi>Qu~;0=A}iGS7Jai}aFa2ZJZzuX)e zG0ISHDts%Y!Jh~lJOEjK0Q?XHL?92ce%~yrEW zz%)=%D6H24S}pMIuocy>q1nyAUKipuUIDq>2qLt$wbQqHZ6sNDPcPj)y==aHd*Ia^7pBh{ zrA$un)`{1cIX4NypeykjuK-VWqtHLN0THR{)kGq}sx=u_t;xV$J4qxHy!7-_05>}m z&lRIc#Ol@tIiUeq)QCO|vmc-EqTuLRmh zt(3wpfc~NS%^%7X;CWGw@b5q&X&OYN+s3mN1&jvTk&7QOE`J9=X0Uz-2#`v3lp;wk z8*TxrSxjXJ%=Ek6#PfC>LN| z8CGin!KLp}Yn8@h1R$AAa?9FvRdy4NRN9{gd21*PD!^JBs8#`=4zy#|{wDsf85km& zbeC@xBY~*R0!{?ln{E86Rlr~TzwZFQkD)aV(9!%aAeHuW0Tr79jsgGh+t1keRr_fU z00)87sLn?b$?Cmn+PvEkA>$|l6i|!=VwnP}^N8F5^rC7l(pIMc)jSJJPvK}!6!rm~ zg}?%e3Q+45!1qtF73xN<1x5hU9bI(y^g3l+oB|?F)&mVvfXM*YZHP_9t46xBm<%mv z1(*oX?_dR3j$#;ulw|g&>g89N0z~@!?d^Ed;RgqRwcsuT3?kJTNQc|z>#hsLD?sE+ z7-w=85o=5ZKxG$z=XtCh7^+je#VGm!fNYE(5E((_G$OC~1=B^S!~zZg3CzlGM?ATi z|9z`7w6?WZz`i=9I|GJjPlo~aVa_YBVeQgW2~xR)LHo0J&V2KOFsy zm2m#*RcfkAi}-uN`8Wlf4YWTTMi7?=>E! z7xwyrDIQz|3Pq8ARNo7d*qq?!C=LGg>u&IX?_kCQzbslB+Y}Jo#5?3lrY7KTfp(_+ zp=}BX@J&qkd)zNIU4Jl#whseyz-alF`SLj?*CgEV|1Z$^y}_k`iCBFYZ@r4UWsOvULUm|$50#BeC zUQH4?f;nv#h1XHNGvvKC;0pfsTLfN1HF6EJcnXL-4tyMV5_15t8}n6Cz(G`_)&tLA z&g(^C53smGVifmz%X?tfAtAr&JR-jUzJ)m;HH-1~v;H-MsbNN9p#_NC101)|PosKE zI4AipSab8}FCp?h;5(ECQMl$X+2|ou&xXERC(12gWeoip%d`EMLA;L<{t~6HF#|jm z`KIb)z+*9eHw5Jt-~m4ZK150QOOyuj0;+RMFJ|Mge4;>;aKk4;A1I*N0yG;pyaN6Y Xf-rA@nJ7jz00000NkvXXu0mjf2jZ=D literal 0 HcmV?d00001 diff --git a/src/bitmaps/manifest.respack b/src/bitmaps/manifest.respack index 2dc110d3a..90af1ce0f 100644 --- a/src/bitmaps/manifest.respack +++ b/src/bitmaps/manifest.respack @@ -536,6 +536,41 @@ button/visual_move_conv_pos_24.png button/visual_move_conv_pos_32.png button/visual_move_conv_pos_48.png button/visual_move_conv_pos_64.png +button/visual_perspective_16.png +button/visual_perspective_24.png +button/visual_perspective_32.png +button/visual_perspective_48.png +button/visual_perspective_64.png +button/visual_perspective_grid_16.png +button/visual_perspective_grid_24.png +button/visual_perspective_grid_32.png +button/visual_perspective_grid_48.png +button/visual_perspective_grid_64.png +button/visual_perspective_lock_outer_16.png +button/visual_perspective_lock_outer_24.png +button/visual_perspective_lock_outer_32.png +button/visual_perspective_lock_outer_48.png +button/visual_perspective_lock_outer_64.png +button/visual_perspective_orgmode_center_16.png +button/visual_perspective_orgmode_center_24.png +button/visual_perspective_orgmode_center_32.png +button/visual_perspective_orgmode_center_48.png +button/visual_perspective_orgmode_center_64.png +button/visual_perspective_orgmode_keep_16.png +button/visual_perspective_orgmode_keep_24.png +button/visual_perspective_orgmode_keep_32.png +button/visual_perspective_orgmode_keep_48.png +button/visual_perspective_orgmode_keep_64.png +button/visual_perspective_orgmode_nofax_16.png +button/visual_perspective_orgmode_nofax_24.png +button/visual_perspective_orgmode_nofax_32.png +button/visual_perspective_orgmode_nofax_48.png +button/visual_perspective_orgmode_nofax_64.png +button/visual_perspective_plane_16.png +button/visual_perspective_plane_24.png +button/visual_perspective_plane_32.png +button/visual_perspective_plane_48.png +button/visual_perspective_plane_64.png button/visual_rotatexy_16.png button/visual_rotatexy_24.png button/visual_rotatexy_32.png diff --git a/src/command/vis_tool.cpp b/src/command/vis_tool.cpp index 8dd221ed6..8079381db 100644 --- a/src/command/vis_tool.cpp +++ b/src/command/vis_tool.cpp @@ -23,6 +23,7 @@ #include "../visual_tool_clip.h" #include "../visual_tool_cross.h" #include "../visual_tool_drag.h" +#include "../visual_tool_perspective.h" #include "../visual_tool_rotatexy.h" #include "../visual_tool_rotatez.h" #include "../visual_tool_scale.h" @@ -68,6 +69,33 @@ namespace { } }; + template + struct visual_tool_persp_setting : public Command { + CMD_TYPE(COMMAND_VALIDATE | COMMAND_TOGGLE) + + bool Validate(const agi::Context *c) override { + return c->videoDisplay->ToolIsType(typeid(VisualToolPerspective)); + } + + virtual const bool CheckActive(int subtool) { + return subtool & M; + } + + virtual const int UpdateSubTool(int subtool) { + return subtool ^ M; + } + + bool IsActive(const agi::Context *c) override { + return Validate(c) && CheckActive(c->videoDisplay->GetSubTool()); + } + + void operator()(agi::Context *c) override { + if (!c->videoDisplay->ToolIsType(typeid(VisualToolPerspective))) + c->videoDisplay->SetTool(agi::make_unique(c->videoDisplay, c)); + c->videoDisplay->SetSubTool(UpdateSubTool(c->videoDisplay->GetSubTool())); + } + }; + struct visual_mode_cross final : public visual_tool_command { CMD_NAME("video/tool/cross") CMD_ICON(visual_standard) @@ -100,6 +128,14 @@ namespace { STR_HELP("Rotate subtitles on their X and Y axes") }; + struct visual_mode_perspective final : public visual_tool_command { + CMD_NAME("video/tool/perspective") + CMD_ICON(visual_perspective) + STR_MENU("Apply 3D Perspective") + STR_DISP("Apply 3D Perspective") + STR_HELP("Rotate and shear subtitles to make them fit a given quad's perspective") + }; + struct visual_mode_scale final : public visual_tool_command { CMD_NAME("video/tool/scale") CMD_ICON(visual_scale) @@ -124,6 +160,113 @@ namespace { STR_HELP("Clip subtitles to a vectorial area") }; + // Perspective settings + struct visual_mode_perspective_plane final : public visual_tool_persp_setting { + CMD_NAME("video/tool/perspective/plane") + CMD_ICON(visual_perspective_plane) + STR_MENU("Show Surrounding Plane") + STR_DISP("Show Surrounding Plane") + STR_HELP("Toggles showing a second quad for the ambient 3D plane.") + }; + + // Perspective settings + struct visual_mode_perspective_lock_inner final : public visual_tool_persp_setting { + CMD_NAME("video/tool/perspective/lock_outer") + CMD_ICON(visual_perspective_lock_outer) + STR_MENU("Lock Outer Quad") + STR_DISP("Lock Outer Quad") + STR_HELP("When the surrounding plane is also visible, switches which quad is locked. If inactive, the inner quad can only be resized without changing the perspective plane. If active, this holds for the outer quad instead.") + + bool Validate(const agi::Context *c) override { + return c->videoDisplay->ToolIsType(typeid(VisualToolPerspective)) && c->videoDisplay->GetSubTool() | PERSP_OUTER; + } + }; + + struct visual_mode_perspective_grid final : public visual_tool_persp_setting { + CMD_NAME("video/tool/perspective/grid") + CMD_ICON(visual_perspective_grid) + STR_MENU("Show Grid") + STR_DISP("Show Grid") + STR_HELP("Toggles showing a 3D grid in the visual perspective tool") + }; + + struct visual_mode_perspective_orgmode_center : public visual_tool_persp_setting { + CMD_NAME("video/tool/perspective/orgmode/center") + CMD_ICON(visual_perspective_orgmode_center) + STR_MENU("\\org Mode: Center") + STR_DISP("\\org Mode: Center") + STR_HELP("Puts \\org at the center of the perspective quad") + + const bool CheckActive(int subtool) override { + return (subtool & PERSP_ORGMODE) == PERSP_ORGMODE_CENTER; + } + + const int UpdateSubTool(int subtool) override { + return (subtool & ~PERSP_ORGMODE) | PERSP_ORGMODE_CENTER; + } + }; + + struct visual_mode_perspective_orgmode_nofax : public visual_tool_persp_setting { + CMD_NAME("video/tool/perspective/orgmode/nofax") + CMD_ICON(visual_perspective_orgmode_nofax) + STR_MENU("\\org Mode: No \\fax") + STR_DISP("\\org Mode: No \\fax") + STR_HELP("Finds a value for \\org where \\fax can be zero, if possible. Use this mode if your event contains line breaks.") + + const bool CheckActive(int subtool) override { + return (subtool & PERSP_ORGMODE) == PERSP_ORGMODE_NOFAX; + } + + const int UpdateSubTool(int subtool) override { + return (subtool & ~PERSP_ORGMODE) | PERSP_ORGMODE_NOFAX; + } + }; + + struct visual_mode_perspective_orgmode_keep : public visual_tool_persp_setting { + CMD_NAME("video/tool/perspective/orgmode/keep") + CMD_ICON(visual_perspective_orgmode_keep) + STR_MENU("\\org Mode: Keep") + STR_DISP("\\org Mode: Keep") + STR_HELP("Fixes the position of \\org") + + const bool CheckActive(int subtool) override { + return (subtool & PERSP_ORGMODE) == PERSP_ORGMODE_KEEP; + } + + const int UpdateSubTool(int subtool) override { + return (subtool & ~PERSP_ORGMODE) | PERSP_ORGMODE_KEEP; + } + }; + + struct visual_mode_perspective_orgmode_cycle : public visual_tool_persp_setting { + CMD_NAME("video/tool/perspective/orgmode/cycle") + STR_MENU("Cycle \\org mode") + STR_DISP("Cycle \\org mode") + STR_HELP("Cycles through the three \\org modes") + + const bool CheckActive(int subtool) override { + return false; + } + + const int UpdateSubTool(int subtool) override { + int newtool = 0; + switch (subtool & PERSP_ORGMODE) { + case PERSP_ORGMODE_CENTER: + newtool = PERSP_ORGMODE_NOFAX; + break; + case PERSP_ORGMODE_NOFAX: + newtool = PERSP_ORGMODE_KEEP; + break; + case PERSP_ORGMODE_KEEP: + newtool = PERSP_ORGMODE_CENTER; + break; + default: + break; + } + return (subtool & ~PERSP_ORGMODE) | newtool; + } + }; + // Vector clip tools struct visual_mode_vclip_drag final : public visual_tool_vclip_command { @@ -191,10 +334,19 @@ namespace cmd { reg(agi::make_unique()); reg(agi::make_unique()); reg(agi::make_unique()); + reg(agi::make_unique()); reg(agi::make_unique()); reg(agi::make_unique()); reg(agi::make_unique()); + reg(agi::make_unique()); + reg(agi::make_unique()); + reg(agi::make_unique()); + reg(agi::make_unique()); + reg(agi::make_unique()); + reg(agi::make_unique()); + reg(agi::make_unique()); + reg(agi::make_unique()); reg(agi::make_unique()); reg(agi::make_unique()); diff --git a/src/libresrc/default_config.json b/src/libresrc/default_config.json index 318f8d3ee..9c0d81244 100644 --- a/src/libresrc/default_config.json +++ b/src/libresrc/default_config.json @@ -574,6 +574,12 @@ "Skip Whitespace" : true }, "Visual" : { + "Perspective": { + "Outer": false, + "Outer Locked": false, + "Grid": false, + "Org Mode": 0 + }, "Autohide": false } }, diff --git a/src/libresrc/default_hotkey.json b/src/libresrc/default_hotkey.json index b9460979e..246141b0d 100644 --- a/src/libresrc/default_hotkey.json +++ b/src/libresrc/default_hotkey.json @@ -338,7 +338,7 @@ "Alt-Left" ], "video/tool/clip" : [ - "H" + "J" ], "video/tool/cross" : [ "A" @@ -346,6 +346,9 @@ "video/tool/drag" : [ "S" ], + "video/tool/perspective": [ + "G" + ], "video/tool/rotate/xy" : [ "F" ], @@ -353,10 +356,10 @@ "D" ], "video/tool/scale" : [ - "G" + "H" ], "video/tool/vector_clip" : [ - "J" + "K" ] } } \ No newline at end of file diff --git a/src/libresrc/default_toolbar.json b/src/libresrc/default_toolbar.json index 07afb90b0..21b95397e 100644 --- a/src/libresrc/default_toolbar.json +++ b/src/libresrc/default_toolbar.json @@ -72,6 +72,7 @@ "video/tool/drag", "video/tool/rotate/z", "video/tool/rotate/xy", + "video/tool/perspective", "video/tool/scale", "video/tool/clip", "video/tool/vector_clip", diff --git a/src/libresrc/osx/default_config.json b/src/libresrc/osx/default_config.json index 59f2ed05f..ae04eb0b2 100644 --- a/src/libresrc/osx/default_config.json +++ b/src/libresrc/osx/default_config.json @@ -574,6 +574,12 @@ "Skip Whitespace" : true }, "Visual" : { + "Perspective": { + "Outer": false, + "Outer Locked": false, + "Grid": false, + "Org Mode": 0 + }, "Autohide": false } }, diff --git a/src/libresrc/osx/default_hotkey.json b/src/libresrc/osx/default_hotkey.json index 829adb88e..57f4a8544 100644 --- a/src/libresrc/osx/default_hotkey.json +++ b/src/libresrc/osx/default_hotkey.json @@ -348,7 +348,7 @@ "Alt-Left" ], "video/tool/clip" : [ - "H" + "J" ], "video/tool/cross" : [ "A" @@ -356,6 +356,9 @@ "video/tool/drag" : [ "S" ], + "video/tool/perspective": [ + "G" + ], "video/tool/rotate/xy" : [ "F" ], @@ -363,10 +366,10 @@ "D" ], "video/tool/scale" : [ - "G" + "H" ], "video/tool/vector_clip" : [ - "J" + "K" ] } } diff --git a/src/meson.build b/src/meson.build index 72587d366..e57a39544 100644 --- a/src/meson.build +++ b/src/meson.build @@ -141,6 +141,7 @@ aegisub_src = files( 'utils.cpp', 'validators.cpp', 'vector2d.cpp', + 'vector3d.cpp', 'version.cpp', 'video_box.cpp', 'video_controller.cpp', @@ -157,6 +158,7 @@ aegisub_src = files( 'visual_tool_clip.cpp', 'visual_tool_cross.cpp', 'visual_tool_drag.cpp', + 'visual_tool_perspective.cpp', 'visual_tool_rotatexy.cpp', 'visual_tool_rotatez.cpp', 'visual_tool_scale.cpp', diff --git a/src/vector3d.cpp b/src/vector3d.cpp new file mode 100644 index 000000000..6b295b8bc --- /dev/null +++ b/src/vector3d.cpp @@ -0,0 +1,99 @@ +// Copyright (c) 2022, arch1t3cht +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// +// Aegisub Project http://www.aegisub.org/ + +/// @file vector3d.cpp +/// @brief 3D mathematical vector used in visual typesetting +/// @ingroup utility visual_ts +/// + +#include "vector3d.h" + +#include "utils.h" + +#include + +#include +#include + +Vector3D::Vector3D() +: x(std::numeric_limits::min()) +, y(std::numeric_limits::min()) +, z(std::numeric_limits::min()) +{ +} + +Vector3D operator *(float f, Vector3D v) { + return Vector3D(v.X() * f, v.Y() * f, v.Z() * f); +} + +Vector3D operator /(float f, Vector3D v) { + return Vector3D(f / v.X(), f / v.Y(), f / v.Z()); +} + +Vector3D operator +(float f, Vector3D v) { + return Vector3D(v.X() + f, v.Y() + f, v.Z() + f); +} + +Vector3D operator -(float f, Vector3D v) { + return Vector3D(f - v.X(), f - v.Y(), f - v.Z()); +} + +Vector3D Vector3D::Unit() const { + float len = Len(); + if (len == 0) + return Vector3D(0, 0, 0); + return *this / len; +} + +Vector3D Vector3D::RotateX(float angle) const { + return Vector3D(x, y * cos(angle) - z * sin(angle), y * sin(angle) + z * cos(angle)); +} + +Vector3D Vector3D::RotateY(float angle) const { + return Vector3D(x * cos(angle) - z * sin(angle), y, x * sin(angle) + z * cos(angle)); +} + +Vector3D Vector3D::RotateZ(float angle) const { + return Vector3D(x * cos(angle) - y * sin(angle), x * sin(angle) + y * cos(angle), z); +} + +Vector3D Vector3D::Max(Vector3D param) const { + return Vector3D(std::max(x, param.x), std::max(y, param.y), std::max(z, param.z)); +} + +Vector3D Vector3D::Min(Vector3D param) const { + return Vector3D(std::min(x, param.x), std::min(y, param.y), std::max(z, param.z)); +} + +Vector3D Vector3D::Round(float step) const { + return Vector3D(floorf(x / step + .5f) * step, floorf(y / step + .5f) * step, floorf(z / step + .5f)); +} + +Vector3D::operator bool() const { + return *this != Vector3D(); +} + +std::string Vector3D::PStr(char sep) const { + return "(" + Str(sep) + ")"; +} + +std::string Vector3D::DStr(char sep) const { + return agi::format("%d%c%d%c%d", (int)x, sep, (int)y, sep, (int)z); +} + +std::string Vector3D::Str(char sep) const { + return float_to_string(x,2) + sep + float_to_string(y,2) + sep + float_to_string(z, 2); +} diff --git a/src/vector3d.h b/src/vector3d.h new file mode 100644 index 000000000..384d29112 --- /dev/null +++ b/src/vector3d.h @@ -0,0 +1,83 @@ +// Copyright (c) 2022, arch1t3cht +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// +// Aegisub Project http://www.aegisub.org/ + +/// @file vector3d.h +/// @see vector3d.cpp +/// @ingroup utility visual_ts +/// + +#pragma once + +#include +#include +#include "vector2d.h" + +class Vector3D { + float x, y, z; + +public: + float X() const { return x; } + float Y() const { return y; } + float Z() const { return z; } + Vector2D XY() const { return Vector2D(x, y); } + + Vector3D(); + Vector3D(Vector2D xy) : x(xy.X()), y(xy.Y()), z(0.) { } + Vector3D(Vector2D xy, float z) : x(xy.X()), y(xy.Y()), z(z) { } + Vector3D(float x, float y, float z) : x(x), y(y), z(z) { } + + bool operator ==(const Vector3D r) const { return x == r.x && y == r.y; } + bool operator !=(const Vector3D r) const { return x != r.x || y != r.y; } + explicit operator bool() const; + + Vector3D operator -() const { return Vector3D(-x, -y, -z); } + Vector3D operator +(const Vector3D r) const { return Vector3D(x + r.x, y + r.y, z + r.z); } + Vector3D operator -(const Vector3D r) const { return Vector3D(x - r.x, y - r.y, z - r.z); } + Vector3D operator *(const Vector3D r) const { return Vector3D(x * r.x, y * r.y, z * r.z); } + Vector3D operator /(const Vector3D r) const { return Vector3D(x / r.x, y / r.y, z / r.z); } + Vector3D operator +(float param) const { return Vector3D(x + param, y + param, z + param); } + Vector3D operator -(float param) const { return Vector3D(x - param, y - param, z - param); } + Vector3D operator *(float param) const { return Vector3D(x * param, y * param, z * param); } + Vector3D operator /(float param) const { return Vector3D(x / param, y / param, z / param); } + + Vector3D Unit() const; + + Vector3D RotateX(float angle) const; + Vector3D RotateY(float angle) const; + Vector3D RotateZ(float angle) const; + + Vector3D Max(Vector3D param) const; + Vector3D Min(Vector3D param) const; + Vector3D Round(float step) const; + + Vector3D Cross(const Vector3D param) const { return Vector3D(y * param.z - z * param.y, z * param.x - x * param.z, x * param.y - y * param.x); } + float Dot(const Vector3D param) const { return x * param.x + y * param.y + z * param.z; } + + float Len() const { return sqrt(x*x + y*y + z*z); } + float SquareLen() const { return x*x + y*y + z*z; } + + /// Get as string with given separator + std::string Str(char sep = ',') const; + /// Get as string surrounded by parentheses with given separator + std::string PStr(char sep = ',') const; + /// Get as string with given separator with values rounded to ints + std::string DStr(char sep = ',') const; +}; + +Vector3D operator * (float f, Vector3D v); +Vector3D operator / (float f, Vector3D v); +Vector3D operator + (float f, Vector3D v); +Vector3D operator - (float f, Vector3D v); diff --git a/src/visual_tool.cpp b/src/visual_tool.cpp index 27b5c4107..e55c3b88f 100644 --- a/src/visual_tool.cpp +++ b/src/visual_tool.cpp @@ -680,4 +680,5 @@ void VisualToolBase::SetOverride(AssDialogue* line, std::string const& tag, std: template class VisualTool; template class VisualTool; template class VisualTool; +template class VisualTool; template class VisualTool; diff --git a/src/visual_tool_perspective.cpp b/src/visual_tool_perspective.cpp new file mode 100644 index 000000000..4bf75ef74 --- /dev/null +++ b/src/visual_tool_perspective.cpp @@ -0,0 +1,894 @@ +// Copyright (c) 2022, arch1t3cht +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// +// Aegisub Project http://www.aegisub.org/ + +/// @file visual_tool_perspective.cpp +/// @brief 3D perspective visual typesetting tool +/// @ingroup visual_ts + +#include "visual_tool_perspective.h" + +#include "command/command.h" +#include "compat.h" +#include "include/aegisub/context.h" +#include "options.h" +#include "selection_controller.h" +#include "vector3d.h" +#include "ass_file.h" +#include "ass_dialogue.h" +#include "ass_style.h" +#include "video_display.h" + +#include +#include +#include + +#include +#include + +static const float pi = 3.1415926536f; +static const float deg2rad = pi / 180.f; +static const float rad2deg = 180.f / pi; +static const float screen_z = 312.5; +static const char *ambient_plane_key = "_aegi_perspective_ambient_plane"; + +static const int BUTTON_ID_BASE = 1400; + +enum VisualToolPerspectiveFeatureType { + FEATURE_INNER = 0, + FEATURE_OUTER = 1, + FEATURE_CENTER = 2, + FEATURE_ORG = 3, +}; + +void Solve2x2(float a11, float a12, float a21, float a22, float b1, float b2, float &x1, float &x2) { + // Simple pivoting + if (abs(a11) < abs(a21)) { + std::swap(b1, b2); + std::swap(a11, a21); + std::swap(a12, a22); + } + // LU decomposition + // i = 1 + a21 = a21 / a11; + // i = 2 + a22 = a22 - a21 * a12; + // forward substitution + float z1 = b1; + float z2 = b2 - a21 * z1; + // backward substitution + x2 = z2 / a22; + x1 = (z1 - a12 * x2) / a11; +} + +Vector2D QuadMidpoint(std::vector quad) { + Vector2D diag1 = quad[2] - quad[0]; + Vector2D diag2 = quad[1] - quad[3]; + Vector2D b = quad[3] - quad[0]; + float center_la1, center_la2; + Solve2x2(diag1.X(), diag2.X(), diag1.Y(), diag2.Y(), b.X(), b.Y(), center_la1, center_la2); + return quad[0] + center_la1 * diag1; +} + +void UnwrapQuadRel(std::vector quad, float &x1, float &x2, float &x3, float &x4, float &y1, float &y2, float &y3, float &y4) { + x1 = quad[0].X(); + x2 = quad[1].X() - x1; + x3 = quad[2].X() - x1; + x4 = quad[3].X() - x1; + y1 = quad[0].Y(); + y2 = quad[1].Y() - y1; + y3 = quad[2].Y() - y1; + y4 = quad[3].Y() - y1; +} + +Vector2D XYToUV(std::vector quad, Vector2D xy) { + float x1, x2, x3, x4, y1, y2, y3, y4; + UnwrapQuadRel(quad, x1, x2, x3, x4, y1, y2, y3, y4); + float x = xy.X() - x1; + float y = xy.Y() - y1; + // Dumped from Mathematica + float u = -(((x3*y2 - x2*y3)*(x4*y - x*y4)*(x4*(-y2 + y3) + x3*(y2 - y4) + x2*(-y3 + y4)))/(x3*x3*(x4*y2*y2*(-y + y4) + y4*(x*y2*(y2 - y4) + x2*(y - y2)*y4)) + x3*(x4*x4*y2*y2*(y - y3) + 2*x4*(x2*y*y3*(y2 - y4) + x*y2*(-y2 + y3)*y4) + x2*y4*(x2*(-y + y3)*y4 + 2*x*y2*(-y3 + y4))) + y3*(x*x4*x4*y2*(y2 - y3) + x2*x4*x4*(y2*y3 + y*(-2*y2 + y3)) - x2*x2*(x4*y*(y3 - 2*y4) + x4*y3*y4 + x*y4*(-y3 + y4))))); + float v = ((x2*y - x*y2)*(x4*y3 - x3*y4)*(x4*(y2 - y3) + x2*(y3 - y4) + x3*(-y2 + y4)))/(x3*(x4*x4*y2*y2*(-y + y3) + x2*y4*(2*x*y2*(y3 - y4) + x2*(y - y3)*y4) - 2*x4*(x2*y*y3*(y2 - y4) + x*y2*(-y2 + y3)*y4)) + x3*x3*(x4*y2*y2*(y - y4) + y4*(x2*(-y + y2)*y4 + x*y2*(-y2 + y4))) + y3*(x*x4*x4*y2*(-y2 + y3) + x2*x4*x4*(2*y*y2 - y*y3 - y2*y3) + x2*x2*(x4*y*(y3 - 2*y4) + x4*y3*y4 + x*y4*(-y3 + y4)))); + return Vector2D(u, v); +} + +Vector2D UVToXY(std::vector quad, Vector2D uv) { + float x1, x2, x3, x4, y1, y2, y3, y4; + UnwrapQuadRel(quad, x1, x2, x3, x4, y1, y2, y3, y4); + float u = uv.X(); + float v = uv.Y(); + // Also dumped from Mathematica + float d = (x4*((-1 + u + v)*y2 + y3 - v*y3) + x3*(y2 - u*y2 + (-1 + v)*y4) + x2*((-1 + u)*y3 - (-1 + u + v)*y4)); + float x = (v*x4*(x3*y2 - x2*y3) + u*x2*(x4*y3 - x3*y4)) / d; + float y = (v*y4*(x3*y2 - x2*y3) + u*y2*(x4*y3 - x3*y4)) / d; + return Vector2D(x + x1, y + y1); +} + +std::vector MakeRect(Vector2D a, Vector2D b) { + return std::vector({ + Vector2D(a.X(), a.Y()), + Vector2D(b.X(), a.Y()), + Vector2D(b.X(), b.Y()), + Vector2D(a.X(), b.Y()), + }); +} + +void VisualToolPerspective::AddTool(std::string command_name, VisualToolPerspectiveSetting setting) { + cmd::Command *command = cmd::get(command_name); + int icon_size = OPT_GET("App/Toolbar Icon Size")->GetInt(); + toolBar->AddTool(BUTTON_ID_BASE + setting, command->StrDisplay(c), command->Icon(icon_size), command->GetTooltip("Video"), wxITEM_CHECK); +} + +VisualToolPerspective::VisualToolPerspective(VideoDisplay *parent, agi::Context *context) +: VisualTool(parent, context) +, optOuter(OPT_SET("Tool/Visual/Perspective/Outer")) +, optOuterLocked(OPT_SET("Tool/Visual/Perspective/Outer Locked")) +, optGrid(OPT_SET("Tool/Visual/Perspective/Grid")) +, optOrgMode(OPT_SET("Tool/Visual/Perspective/Org Mode")) +{ + old_outer.resize(4); + old_inner.resize(4); + + settings = 0; + if (optOuter->GetBool()) settings |= PERSP_OUTER; + if (optOuterLocked->GetBool()) settings |= PERSP_LOCK_OUTER; + if (optGrid->GetBool()) settings |= PERSP_GRID; + settings |= optOrgMode->GetInt(); + + MakeFeatures(); +} + +void VisualToolPerspective::SetToolbar(wxToolBar *toolBar) { + this->toolBar = toolBar; + + toolBar->AddSeparator(); + + AddTool("video/tool/perspective/plane", PERSP_OUTER); + AddTool("video/tool/perspective/lock_outer", PERSP_LOCK_OUTER); + AddTool("video/tool/perspective/grid", PERSP_GRID); + AddTool("video/tool/perspective/orgmode/center", PERSP_ORGMODE); + + SetSubTool(settings); + + toolBar->Realize(); + toolBar->Show(true); + toolBar->Bind(wxEVT_TOOL, &VisualToolPerspective::OnSubTool, this); +} + +void VisualToolPerspective::OnSubTool(wxCommandEvent &e) { + int id = e.GetId() - BUTTON_ID_BASE; + if (id == PERSP_ORGMODE) { + cmd::call("video/tool/perspective/orgmode/cycle", c); + } else { + SetSubTool(GetSubTool() ^ id); + } +} + +void VisualToolPerspective::SetSubTool(int subtool) { + if (toolBar == nullptr) { + throw agi::InternalError("Vector clip toolbar hasn't been set yet!"); + } + for (int i = 1; i < PERSP_LAST; i <<= 1) + toolBar->ToggleTool(BUTTON_ID_BASE + i, i & subtool); + + toolBar->EnableTool(BUTTON_ID_BASE + PERSP_LOCK_OUTER, subtool & PERSP_OUTER); + + cmd::Command *orgmode; + switch (subtool & PERSP_ORGMODE) { + case PERSP_ORGMODE_CENTER: + orgmode = cmd::get("video/tool/perspective/orgmode/center"); + break; + case PERSP_ORGMODE_NOFAX: + orgmode = cmd::get("video/tool/perspective/orgmode/nofax"); + break; + case PERSP_ORGMODE_KEEP: + orgmode = cmd::get("video/tool/perspective/orgmode/keep"); + break; + default: + throw agi::InternalError("Invalid perspective subtool"); + } + wxString orgmodehelp = orgmode->StrDisplay(c) + wxString(". Click to cycle.\n") + orgmode->GetTooltip("Video"); + toolBar->SetToolShortHelp(BUTTON_ID_BASE + PERSP_ORGMODE, orgmodehelp); + toolBar->SetToolLongHelp(BUTTON_ID_BASE + PERSP_ORGMODE, orgmodehelp); + toolBar->SetToolNormalBitmap(BUTTON_ID_BASE + PERSP_ORGMODE, orgmode->Icon(OPT_GET("App/Toolbar Icon Size")->GetInt())); + toolBar->ToggleTool(BUTTON_ID_BASE + PERSP_ORGMODE, false); + + settings = subtool; + + optOuter->SetBool(HasOuter()); + optOuterLocked->SetBool(OuterLocked()); + optGrid->SetBool(settings & PERSP_GRID); + optOrgMode->SetInt(GetOrgMode()); + + MakeFeatures(); + parent->Render(); +} + +int VisualToolPerspective::GetSubTool() { + return settings; +} + +bool VisualToolPerspective::HasOuter() { + return GetSubTool() & PERSP_OUTER; +} + +bool VisualToolPerspective::OuterLocked() { + return HasOuter() && (GetSubTool() & PERSP_LOCK_OUTER); +} + +int VisualToolPerspective::GetOrgMode() { + return GetSubTool() & PERSP_ORGMODE; +} + +bool VisualToolPerspective::HasOrgf() { + return GetOrgMode() == PERSP_ORGMODE_KEEP; +} + +std::vector VisualToolPerspective::FeaturePositions(std::vector features) const { + std::vector result; + for (size_t i = 0; i < 4; i++) { + result.push_back(features[i]->pos); + } + return result; +} + +void VisualToolPerspective::UpdateInner() { + std::vector uv = MakeRect(c1, c2); + std::vector quad = FeaturePositions(outer_corners); + for (int i = 0; i < 4; i++) + inner_corners[i]->pos = UVToXY(quad, uv[i]); +} + +void VisualToolPerspective::UpdateOuter() { + if (!HasOuter()) + return; + std::vector uv = MakeRect(-c1 / (c2 - c1), (1 - c1) / (c2 - c1)); + std::vector quad = FeaturePositions(inner_corners); + for (int i = 0; i < 4; i++) + outer_corners[i]->pos = UVToXY(quad, uv[i]); +} + +void VisualToolPerspective::MakeFeatures() { + sel_features.clear(); + features.clear(); + active_feature = nullptr; + + inner_corners.clear(); + outer_corners.clear(); + orgf = nullptr; + + centerf = new Feature(this, FEATURE_CENTER, 0); + centerf->type = DRAG_BIG_TRIANGLE; + features.push_back(*centerf); + + if (HasOrgf()) { + orgf = new Feature(this, FEATURE_ORG, 0); + orgf->type = DRAG_BIG_TRIANGLE; + features.push_back(*orgf); + } + + for (int i = 0; i < 4; i++) { + inner_corners.push_back(new Feature(this, FEATURE_INNER, i)); + inner_corners.back()->type = DRAG_SMALL_CIRCLE; + features.push_back(*inner_corners.back()); + + if (HasOuter()) { + outer_corners.push_back(new Feature(this, FEATURE_OUTER, i)); + outer_corners.back()->type = DRAG_SMALL_CIRCLE; + features.push_back(*outer_corners.back()); + } + } + + DoRefresh(); +} + +void VisualToolPerspective::Draw() { + if (!active_line) return; + + wxColour line_color = to_wx(line_color_primary_opt->GetColor()); + wxColour line_color_secondary = to_wx(line_color_secondary_opt->GetColor()); + + // Draw Quad + gl.SetLineColour(line_color); + for (int i = 0; i < 4; i++) { + if (HasOuter()) { + gl.DrawDashedLine(outer_corners[i]->pos, outer_corners[(i + 1) % 4]->pos, 6); + gl.DrawLine(inner_corners[i]->pos, inner_corners[(i + 1) % 4]->pos); + } else { + gl.DrawDashedLine(inner_corners[i]->pos, inner_corners[(i + 1) % 4]->pos, 6); + } + } + + DrawAllFeatures(); + + if (GetSubTool() & PERSP_GRID) { + // Draw Grid - Copied and modified from visual_tool_rotatexy.cpp + + // Number of lines on each side of each axis + static const int radius = 15; + // Total number of lines, including center axis line + static const int line_count = radius * 2 + 1; + // Distance between each line in pixels + static const int spacing = 20; + // Length of each grid line in pixels from axis to one end + static const int half_line_length = spacing * (radius + 1); + static const float fade_factor = 0.9f / radius; + + // Transform grid + gl.SetOrigin(FromScriptCoords(org)); + gl.SetScale(100 * video_res / script_res); + gl.SetRotation(angle_x, angle_y, angle_z); + gl.SetScale(fsc); + gl.SetShear(fax, fay); + Vector2D glScale = textheight * Vector2D(1, 1) / spacing / 4; + gl.SetScale(100 * glScale); + + // Draw grid + gl.SetLineColour(line_color_secondary, 0.5f, 2); + gl.SetModeLine(); + float r = line_color_secondary.Red() / 255.f; + float g = line_color_secondary.Green() / 255.f; + float b = line_color_secondary.Blue() / 255.f; + + std::vector colors(line_count * 8 * 4); + for (int i = 0; i < line_count * 8; ++i) { + colors[i * 4 + 0] = r; + colors[i * 4 + 1] = g; + colors[i * 4 + 2] = b; + colors[i * 4 + 3] = (i + 3) % 4 > 1 ? 0 : (1.f - abs(i / 8 - radius) * fade_factor); + } + + std::vector points(line_count * 8 * 2); + for (int i = 0; i < line_count; ++i) { + int pos = spacing * (i - radius); + + points[i * 16 + 0] = pos; + points[i * 16 + 1] = half_line_length; + + points[i * 16 + 2] = pos; + points[i * 16 + 3] = 0; + + points[i * 16 + 4] = pos; + points[i * 16 + 5] = 0; + + points[i * 16 + 6] = pos; + points[i * 16 + 7] = -half_line_length; + + points[i * 16 + 8] = half_line_length; + points[i * 16 + 9] = pos; + + points[i * 16 + 10] = 0; + points[i * 16 + 11] = pos; + + points[i * 16 + 12] = 0; + points[i * 16 + 13] = pos; + + points[i * 16 + 14] = -half_line_length; + points[i * 16 + 15] = pos; + } + + Vector2D offset = (ToScriptCoords(QuadMidpoint(FeaturePositions(inner_corners))) - org) / glScale; + for (int i = 0; i < line_count * 8; ++i) { + points[i * 2 + 0] += offset.X(); + points[i * 2 + 1] += offset.Y(); + } + + gl.DrawLines(2, points, 4, colors); + + gl.ResetTransform(); + } +} + +void VisualToolPerspective::OnDoubleClick() { + std::vector active_features = (HasOuter() && !OuterLocked()) ? outer_corners : inner_corners; + int maxi = -1; + float mind = -1; + for (size_t i = 0; i < active_features.size(); i++) { + float d = (active_features[i]->pos - mouse_pos).Len(); + if (maxi == -1 || d < mind) { + maxi = i; + mind = d; + } + } + active_features[maxi]->pos = mouse_pos; + UpdateDrag(active_features[maxi]); + Commit(); +} + +void VisualToolPerspective::OnMouseEvent(wxMouseEvent &event) { + // Override this so we can find out which modifier keys were held + shift_down = event.ShiftDown(); + ctrl_down = event.CmdDown(); + alt_down = event.AltDown(); + VisualTool::OnMouseEvent(event); + shift_down = false; + ctrl_down = false; + alt_down = false; +}; + +void VisualToolPerspective::UpdateDrag(Feature *feature) { + if (feature == centerf) { + Vector2D oldCenter = QuadMidpoint(FeaturePositions(inner_corners)); + if (HasOuter() && !OuterLocked()) { + std::vector quad = FeaturePositions(outer_corners); + Vector2D olduv = XYToUV(quad, oldCenter); + Vector2D newuv = XYToUV(quad, centerf->pos); + c1 = c1 + newuv - olduv; + c2 = c2 + newuv - olduv; + UpdateInner(); + } else { + Vector2D diff = centerf->pos - oldCenter; + for (int i = 0; i < 4; i++) { + inner_corners[i]->pos = inner_corners[i]->pos + diff; + } + UpdateOuter(); + } + } else if (HasOrgf() && feature == orgf) { + org = ToScriptCoords(feature->pos); + } + + std::vector changed_quad; + std::vector changed_quad_old; + if (feature->group == FEATURE_INNER) { + changed_quad = inner_corners; + changed_quad_old = old_inner; + } else if (HasOuter() && feature->group == FEATURE_OUTER) { + changed_quad = outer_corners; + changed_quad_old = old_outer; + } + + if (!changed_quad.empty() && !ctrl_down) { + // Validate: If the quad isn't convex, the intersection of the diagonals will not lie inside it. + Vector2D diag1 = changed_quad[2]->pos - changed_quad[0]->pos; + Vector2D diag2 = changed_quad[1]->pos - changed_quad[3]->pos; + Vector2D b = changed_quad[3]->pos - changed_quad[0]->pos; + float center_la1, center_la2; + Solve2x2(diag1.X(), diag2.X(), diag1.Y(), diag2.Y(), b.X(), b.Y(), center_la1, center_la2); + if (center_la1 < 0 || center_la1 > 1 || -center_la2 < 0 || -center_la2 > 1) { + TextToPersp(); + return; + } + } + + int i = feature->index; + + if (ctrl_down && !changed_quad.empty()) { + if (alt_down) { + if (shift_down) { + int bestsnap = -1; + float mindist = -1; + for (int j = 0; j < 4; j++) { + float dist = (feature->pos - changed_quad_old[j]).SquareLen(); + if (bestsnap == -1 || dist < mindist) { + bestsnap = j; + mindist = dist; + } + } + feature->pos = changed_quad_old[bestsnap]; + } else { + Vector2D center = QuadMidpoint(changed_quad_old); + Vector2D diff = feature->pos - center; + Vector2D snapDirection1 = (changed_quad_old[0] - center).Unit(); + Vector2D snapDirection2 = (changed_quad_old[1] - center).Unit(); + Vector2D snap1 = diff.Dot(snapDirection1) * snapDirection1; + Vector2D snap2 = diff.Dot(snapDirection2) * snapDirection2; + diff = (snap1 - diff).SquareLen() <= (snap2 - diff).SquareLen() ? snap1 : snap2; + feature->pos = center + diff; + } + } + + Vector2D relUV = XYToUV(changed_quad_old, feature->pos) - Vector2D(0.5, 0.5); + + for (int j = 0; j < 4; j++) { + Vector2D flipi(i == 1 || i == 2 ? -1 : 1, i >= 2 ? -1 : 1); + Vector2D flipj(j == 1 || j == 2 ? -1 : 1, j >= 2 ? -1 : 1); + changed_quad[j]->pos = UVToXY(changed_quad_old, Vector2D(0.5, 0.5) + relUV * flipi * flipj); + } + + if (HasOuter()) { + if (feature->group == FEATURE_INNER) { + if (!OuterLocked()) { + c1 = XYToUV(FeaturePositions(outer_corners), inner_corners[0]->pos); + c2 = XYToUV(FeaturePositions(outer_corners), inner_corners[2]->pos); + UpdateInner(); + } else { + UpdateOuter(); + } + } else if (feature->group == FEATURE_OUTER) { + if (OuterLocked()) { + c1 = XYToUV(FeaturePositions(outer_corners), inner_corners[0]->pos); + c2 = XYToUV(FeaturePositions(outer_corners), inner_corners[2]->pos); + UpdateOuter(); + } else { + UpdateInner(); + } + } + } + } else if (!changed_quad.empty() && HasOuter()) { + // Normally dragging one corner + if (feature->group == FEATURE_INNER) { + if (!OuterLocked()) { + Vector2D newuv = XYToUV(FeaturePositions(outer_corners), feature->pos); + c1 = Vector2D(i == 0 || i == 3 ? newuv.X() : c1.X(), i < 2 ? newuv.Y() : c1.Y()); + c2 = Vector2D(i == 0 || i == 3 ? c2.X() : newuv.X(), i < 2 ? c2.Y() : newuv.Y()); + UpdateInner(); + } else { + UpdateOuter(); + } + } else if (feature->group == FEATURE_OUTER) { + if (OuterLocked()) { + Vector2D d1 = -c1 / (c2 - c1); + Vector2D d2 = (1 - c1) / (c2 - c1); + Vector2D newuv = XYToUV(FeaturePositions(inner_corners), feature->pos); + d1 = Vector2D(i == 0 || i == 3 ? newuv.X() : d1.X(), i < 2 ? newuv.Y() : d1.Y()); + d2 = Vector2D(i == 0 || i == 3 ? d2.X() : newuv.X(), i < 2 ? d2.Y() : newuv.Y()); + c1 = -d1 / (d2 - d1); + c2 = (1 - d1) / (d2 - d1); + UpdateOuter(); + } else { + UpdateInner(); + } + } + } + + if (!InnerToText()) + TextToPersp(); + SetFeaturePositions(); +} + +void VisualToolPerspective::EndDrag(Feature *feature) { + SaveFeaturePositions(); + SaveOuterToLines(); +} + +void VisualToolPerspective::WrapSetOverride(AssDialogue* line, std::string const& tag, float value, int precision, float defaultval) { + std::string format = agi::format("%%.%df", precision); + std::string formatted = agi::format(format.c_str(), value); + std::string default_formatted = agi::format(format.c_str(), defaultval); + if (formatted == default_formatted || (defaultval == 0 && agi::format(format.c_str(), -value) == default_formatted)) + RemoveOverride(line, tag); + else + SetOverride(line, tag, formatted); +} + +bool VisualToolPerspective::InnerToText() { + Vector2D q0 = ToScriptCoords(inner_corners[0]->pos); + Vector2D q1 = ToScriptCoords(inner_corners[1]->pos); + Vector2D q2 = ToScriptCoords(inner_corners[2]->pos); + Vector2D q3 = ToScriptCoords(inner_corners[3]->pos); + + // Find a parallelogram projecting to the quad. This is independent of translation. + float z1, z3; + Vector2D diag = q2 - q0; + Vector2D side2 = q1 - q2; + Vector2D side3 = q3 - q2; + Solve2x2(side2.X(), side3.X(), side2.Y(), side3.Y(), -diag.X(), -diag.Y(), z1, z3); + + Vector2D midpoint = QuadMidpoint(std::vector({q0, q1, q2, q3})); + + if (GetOrgMode() == PERSP_ORGMODE_CENTER) { + org = midpoint; + } else if (GetOrgMode() == PERSP_ORGMODE_NOFAX) { + Vector2D v1 = q1 - q0; + Vector2D v3 = q3 - q0; + // Look for a translation after which the quad will unproject to a rectangle. + // Specifically, look for a vector t such that this happens after moving q0 to t. + // The set of such vectors is cut out by the equation a (x^2 + y^2) - b1 x - b2 y + c + // with the following coefficients. + float a = (1 - z1) * (1 - z3); + Vector2D b = z1 * v1 + z3 * v3 - z1 * z3 * (v1 + v3); + float c = z1 * z3 * v1.Dot(v3) + (z1 - 1) * (z3 - 1) * screen_z * screen_z; + + // Our default value for t, which would put \org at the center of the quad. + // We'll try to find a value for \org that's as close as possible to it. + Vector2D t = q0 - midpoint; + + // Handle all the edge cases. These can actually come up in practice, like when + // starting from text without any perspective. + if (a == 0) { + // If b = 0 we get a trivial or impossible equation, so just keep the previous \org. + if (b.SquareLen() != 0) { + // The equation cuts out a line. Find the point closest to the previous t. + t = t + b * ((c - t.Dot(b)) / b.SquareLen()); + } + } else { + // The equation cuts out a circle. + // Complete the square to find center and radius. + Vector2D circleCenter = b / (2 * a); + float sqradius = (b.SquareLen() / (4 * a) - c) / a; + + if (sqradius <= 0) { + // This is actually very rare. + org = circleCenter; + } else { + // Find the point on the circle closest to the current \org. + float radius = sqrt(sqradius); + Vector2D center2t = t - circleCenter; + if (center2t.Len() == 0) { + t = circleCenter + Vector2D(radius, 0); + } else { + t = circleCenter + center2t / center2t.Len() * radius; + } + } + } + + org = q0 - t; + } + + // Normalize to org + q0 = q0 - org; + q1 = q1 - org; + q2 = q2 - org; + q3 = q3 - org; + + Vector3D r0 = Vector3D(q0, screen_z); + Vector3D r1 = z1 * Vector3D(q1, screen_z); + Vector3D r2 = (z1 + z3 - 1) * Vector3D(q2, screen_z); + Vector3D r3 = z3 * Vector3D(q3, screen_z); + std::vector r({r0, r1, r2, r3}); + + // Find the z coordinate of the point projecting to the origin + float orgla0, orgla1; + Vector3D side0 = r1 - r0; + Vector3D side1 = r3 - r0; + Solve2x2(side0.X(), side1.X(), side0.Y(), side1.Y(), -r0.X(), -r0.Y(), orgla0, orgla1); + float orgz = (r0 + orgla0 * side0 + orgla1 * side1).Z(); + + // Normalize so the origin has z=screen_z, and move the screen plane to z=0 + for (int i = 0; i < 4; i++) + r[i] = r[i] * screen_z / orgz - Vector3D(0, 0, screen_z); + + // Find the rotations + Vector3D n = (r[1] - r[0]).Cross(r[3] - r[0]); + float roty = atan(n.X() / n.Z()); + if (n.Z() < 0) + roty += pi; + n = n.RotateY(roty); + float rotx = atan(n.Y() / n.Z()); + + // Rotate into the z=0 plane + for (int i = 0; i < 4; i++) + r[i] = r[i].RotateY(roty).RotateX(rotx); + + Vector3D ab = r[1] - r[0]; + float rotz = atan(ab.Y() / ab.X()); + if (ab.X() < 0) + rotz += pi; + + // Rotate to make the top side be horizontal + for (int i = 0; i < 4; i++) + r[i] = r[i].RotateZ(-rotz); + + // We now have a horizontal parallelogram in the plane, so find the shear and the dimensions + ab = r[1] - r[0]; + Vector3D ad = r[3] - r[0]; + float rawfax = ad.X() / ad.Y(); + + float quadwidth = ab.Len(); + float quadheight = abs(ad.Y()); + float scalex = quadwidth / textwidth; + float scaley = quadheight / textheight; + + float shiftv = align <= 3 ? 1 : (align <= 6 ? 0.5 : 0); + float shifth = align % 3 == 0 ? 1 : (align % 3 == 2 ? 0.5 : 0); + pos = org + r[0].XY() + Vector2D(quadwidth * shifth, quadheight * shiftv); + angle_x = rotx * rad2deg; + angle_y = -roty * rad2deg; + angle_z = -rotz * rad2deg; + Vector2D oldfsc = fsc; + fsc = 100 * Vector2D(scalex, scaley); + fax = rawfax * scaley / scalex; + fay = 0; + + bord = bord * fsc / oldfsc; + shad = shad * fsc / oldfsc; + + // Give up if any of these numbers were invalid + std::vector allvalues({fax, fsc.X(), fsc.Y(), angle_z, angle_x, angle_y, bord.X(), bord.Y(), shad.X(), shad.Y(), org.X(), org.Y(), pos.X(), pos.Y()}); + for (float f : allvalues) { + if (!isfinite(f)) return false; + } + + for (auto line : c->selectionController->GetSelectedSet()) { + auto style = c->ass->GetStyle(line->Style); + // Maybe just set the tags manually so the line doesn't need to be parsed again for every tag? + WrapSetOverride(line, "\\fax", fax, 6); + WrapSetOverride(line, "\\fay", 0, 6); + WrapSetOverride(line, "\\fscx", fsc.X(), 2, style->scalex); + WrapSetOverride(line, "\\fscy", fsc.Y(), 2, style->scaley); + WrapSetOverride(line, "\\frz", angle_z, 4, style->angle); + WrapSetOverride(line, "\\frx", angle_x, 4); + WrapSetOverride(line, "\\fry", angle_y, 4); + RemoveOverride(line, "\\bord"); + RemoveOverride(line, "\\shad"); + WrapSetOverride(line, "\\xbord", bord.X(), 2, style->outline_w); + WrapSetOverride(line, "\\ybord", bord.Y(), 2, style->outline_w); + WrapSetOverride(line, "\\xshad", shad.X(), 2, style->shadow_w); + WrapSetOverride(line, "\\yshad", shad.Y(), 2, style->shadow_w); + SetOverride(line, "\\org", org.PStr()); + SetOverride(line, "\\pos", pos.PStr()); + } + return true; +} + +void VisualToolPerspective::SaveFeaturePositions() { + for (int i = 0; i < 4; i++) { + old_inner[i] = inner_corners[i]->pos; + if (HasOuter()) + old_outer[i] = outer_corners[i]->pos; + } +} + +void VisualToolPerspective::SaveOuterToLines() { + if (HasOuter()) { + std::string plane_descriptor; + for (int i = 0; i < 4; i++) { + Vector2D saved_corner = ToScriptCoords(outer_corners[i]->pos); + if (!isfinite(saved_corner.X()) || !isfinite(saved_corner.Y())) + return; + plane_descriptor += agi::format("%.2f;%.2f", saved_corner.X(), saved_corner.Y()); + if (i < 3) plane_descriptor += "|"; + } + uint32_t plane_extra = c->ass->AddExtradata(ambient_plane_key, plane_descriptor); + + for (auto line : c->selectionController->GetSelectedSet()) { + // Let's reinvent the wheel a bit since extradata tooling is nonexistent + std::vector extra = line->ExtradataIds.get(); + std::vector entries = c->ass->GetExtradata(extra); + for (int i = entries.size() - 1; i >= 0; i--) { + if (entries[i].key == ambient_plane_key) + extra.erase(extra.begin() + i, extra.begin() + i + 1); + } + extra.push_back(plane_extra); + line->ExtradataIds = extra; + } + } +} + +void VisualToolPerspective::SetFeaturePositions() { + centerf->pos = QuadMidpoint(FeaturePositions(inner_corners)); + if (orgf != nullptr) + orgf->pos = FromScriptCoords(org); +} + +void VisualToolPerspective::TextToPersp() { + if (!active_line) return; + + org = GetLineOrigin(active_line); + pos = GetLinePosition(active_line); + if (!org) + org = pos; + + GetLineRotation(active_line, angle_x, angle_y, angle_z); + GetLineShear(active_line, fax, fay); + GetLineScale(active_line, fsc); + GetLineOutline(active_line, bord); + GetLineShadow(active_line, shad); + + align = GetLineAlignment(active_line); + + double descend, extlead; + GetLineBaseExtents(active_line, textwidth, textheight, descend, extlead); + textwidth = std::max(textwidth, 1.); + textheight = std::max(textheight, 1.); + double textleft, texttop = 0.; + + switch ((align - 1) % 3) { + case 1: + textleft = -textwidth / 2; + break; + case 2: + textleft = -textwidth; + break; + default: + break; + } + switch ((align - 1) / 3) { + case 0: + texttop = -textheight; + break; + case 1: + texttop = -textheight / 2; + break; + default: + break; + } + + std::vector textrect = MakeRect(Vector2D(0, 0), Vector2D(textwidth, textheight)); + for (int i = 0; i < 4; i++) { + Vector2D p = textrect[i]; + // Apply \fax and \fay + p = Vector2D(p.X() + p.Y() * fax, p.X() * fay + p.Y()); + // Translate to alignment point + p = p + Vector2D(textleft, texttop); + // Apply scaling + p = Vector2D(p.X() * fsc.X() / 100., p.Y() * fsc.Y() / 100.); + // Translate relative to origin + p = p + pos - org; + // Rotate ZXY + Vector3D q(p); + q = q.RotateZ(-angle_z * deg2rad); + q = q.RotateX(-angle_x * deg2rad); + q = q.RotateY(angle_y * deg2rad); + // Project + q = (screen_z / (q.Z() + screen_z)) * q; + // Move to origin + Vector2D r = q.XY() + org; + inner_corners[i]->pos = FromScriptCoords(r); + } + + for (auto const& extra : c->ass->GetExtradata(active_line->ExtradataIds)) { + if (extra.key == ambient_plane_key) { + std::vector fields; + agi::Split(fields, extra.value, '|'); + if (fields.size() != 4) + break; + + std::vector saved_outer; + for (int i = 0; i < 4; i++) { + std::vector ordinates; + agi::Split(ordinates, fields[i], ';'); + if (ordinates.size() != 2) + break; + + double x, y; + if (!agi::util::try_parse(ordinates[0], &x)) break; + if (!agi::util::try_parse(ordinates[1], &y)) break; + + saved_outer.emplace_back(x, y); + } + if (saved_outer.size() != 4) break; + + Vector2D d1 = XYToUV(saved_outer, ToScriptCoords(inner_corners[0]->pos)); + Vector2D d2 = XYToUV(saved_outer, ToScriptCoords(inner_corners[2]->pos)); + if (isfinite(d1.X()) && isfinite(d1.Y()) && isfinite(d2.X()) && isfinite(d2.Y())) { + c1 = d1; + c2 = d2; + } + } + } + + UpdateOuter(); +} + +void VisualToolPerspective::DoRefresh() { + TextToPersp(); + SetFeaturePositions(); + SaveFeaturePositions(); +} + +VisualToolPerspectiveDraggableFeature::VisualToolPerspectiveDraggableFeature(VisualToolPerspective *tool, int group, int index) : tool(tool), group(group), index(index) {} + +void VisualToolPerspectiveDraggableFeature::UpdateDrag(Vector2D d, bool single_axis) { + if (tool->ctrl_down && tool->alt_down) + single_axis = false; // This is handled manually later on + + if (single_axis && !(group == FEATURE_CENTER && !(tool->HasOuter() && !tool->OuterLocked()))) { + // Snap to the axes *inside* of the quad's perspective plane. + std::vector quad = tool->old_inner; + Vector2D posUV = XYToUV(quad, pos); + Vector2D axis1 = UVToXY(quad, posUV + Vector2D(1, 0)) - pos; + Vector2D axis2 = UVToXY(quad, posUV + Vector2D(0, 1)) - pos; + + // Normalize and project + axis1 = axis1.Unit(); + axis2 = axis2.Unit(); + Vector2D snap1 = d.Dot(axis1) * axis1; + Vector2D snap2 = d.Dot(axis2) * axis2; + d = (snap1 - d).SquareLen() <= (snap2 - d).SquareLen() ? snap1 : snap2; + single_axis = false; + } + VisualDraggableFeature::UpdateDrag(d, single_axis); +} diff --git a/src/visual_tool_perspective.h b/src/visual_tool_perspective.h new file mode 100644 index 000000000..82647a7b6 --- /dev/null +++ b/src/visual_tool_perspective.h @@ -0,0 +1,135 @@ +// Copyright (c) 2022, arch1t3cht +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// +// Aegisub Project http://www.aegisub.org/ + +/// @file visual_tool_perspective.h +/// @see visual_tool_perspective.cpp +/// @ingroup visual_ts +/// + +#include "visual_feature.h" +#include "visual_tool.h" +#include "options.h" + +class wxToolBar; + +/// Button IDs +enum VisualToolPerspectiveSetting { + PERSP_OUTER = 1 << 0, + PERSP_LOCK_OUTER = 1 << 1, + PERSP_GRID = 1 << 2, + PERSP_LAST = 1 << 3, // End of simple toggle-able options + PERSP_ORGMODE_CENTER = 0 << 4, // Always puts \org at the center of the quad. Default. + PERSP_ORGMODE_NOFAX = 1 << 4, // Picks a position for \org where \fax = 0, when possible + PERSP_ORGMODE_KEEP = 2 << 4, // Takes the previous \org position as \org + PERSP_ORGMODE = PERSP_ORGMODE_CENTER | PERSP_ORGMODE_NOFAX | PERSP_ORGMODE_KEEP, +}; + +class VisualToolPerspective; + +class VisualToolPerspectiveDraggableFeature final : public VisualDraggableFeature { + VisualToolPerspective *tool; + +public: + int group; + int index; + + VisualToolPerspectiveDraggableFeature(VisualToolPerspective *tool, int group, int index); + void UpdateDrag(Vector2D d, bool single_axis); +}; + +class VisualToolPerspective final : public VisualTool { + wxToolBar *toolBar = nullptr; /// The subtoolbar + int settings = 0; + + agi::OptionValue* optOuter; + agi::OptionValue* optOuterLocked; + agi::OptionValue* optGrid; + agi::OptionValue* optOrgMode; + + // All current transform coefficients. Used for drawing the grid. + float angle_x = 0.f; + float angle_y = 0.f; + float angle_z = 0.f; + + float fax = 0.f; + float fay = 0.f; + + int align = 0; + + double textwidth = 0.f; + double textheight = 0.f; + + Vector2D fsc; + + Vector2D org; + Vector2D pos; + + // Store these here to reduce rounding errors compounding on updates + Vector2D bord; + Vector2D shad; + + // Corner coordinates of the transform quad relative to the ambient quad. + Vector2D c1 = Vector2D(.25, .25); + Vector2D c2 = Vector2D(.75, .75); + + Feature *centerf; + Feature *orgf; + Vector2D old_centerf; + + std::vector inner_corners; + std::vector outer_corners; + + std::vector FeaturePositions(std::vector features) const; + void UpdateInner(); + void UpdateOuter(); + void TextToPersp(); + bool InnerToText(); + + void WrapSetOverride(AssDialogue* line, std::string const& tag, float value, int precision, float defaultval=0); + + void OnMouseEvent(wxMouseEvent &event) override; + void DoRefresh() override; + void Draw() override; + void OnDoubleClick() override; + void UpdateDrag(Feature *feature) override; + void EndDrag(Feature *feature) override; + void MakeFeatures(); + void SetFeaturePositions(); + void SaveFeaturePositions(); + void SaveOuterToLines(); + + void AddTool(std::string command_name, VisualToolPerspectiveSetting mode); + +public: + bool ctrl_down = false; + bool shift_down = false; + bool alt_down = false; + + std::vector old_inner; + std::vector old_outer; + + VisualToolPerspective(VideoDisplay *parent, agi::Context *context); + + bool HasOuter(); + bool OuterLocked(); + int GetOrgMode(); + bool HasOrgf(); + + void SetToolbar(wxToolBar *tb) override; + void OnSubTool(wxCommandEvent &); + void SetSubTool(int subtool) override; + int GetSubTool() override; +}; diff --git a/src/visual_tool_rotatexy.h b/src/visual_tool_rotatexy.h index a4c00a373..86c4803b8 100644 --- a/src/visual_tool_rotatexy.h +++ b/src/visual_tool_rotatexy.h @@ -29,6 +29,7 @@ class VisualToolRotateXY final : public VisualTool { float fax = 0.f; float fay = 0.f; + Vector2D fsc; float orig_x = 0.f; ///< x rotation at the beginning of the current hold float orig_y = 0.f; ///< y rotation at the beginning of the current hold