From a683cce9b25c597b08a3f313f39091bb51de55e4 Mon Sep 17 00:00:00 2001 From: derfies Date: Fri, 2 Nov 2012 05:14:20 +1100 Subject: [PATCH] Initial commit. --- .gitignore | 1 + src/data/images/arrow-curve-flip.png | Bin 0 -> 3666 bytes src/data/images/arrow-curve.png | Bin 0 -> 929 bytes src/data/images/disk-black-pencil.png | Bin 0 -> 3495 bytes src/data/images/disk-black.png | Bin 0 -> 595 bytes src/data/images/document.png | Bin 0 -> 549 bytes src/data/images/folder-horizontal-open.png | Bin 0 -> 3391 bytes src/main.py | 8 + src/p3d/__init__.py | 32 + src/p3d/camera.py | 202 ++++++ src/p3d/commonUtils.py | 71 ++ src/p3d/constants.py | 6 + src/p3d/displayShading.py | 30 + src/p3d/editorCamera.py | 43 ++ src/p3d/frameRate.py | 15 + src/p3d/functions.py | 27 + src/p3d/geometry.py | 333 ++++++++++ src/p3d/marquee.py | 79 +++ src/p3d/mouse.py | 63 ++ src/p3d/mousePicker.py | 98 +++ src/p3d/nodePathObject.py | 26 + src/p3d/object.py | 31 + src/p3d/pandaBehaviour.py | 29 + src/p3d/pandaManager.py | 53 ++ src/p3d/pandaObject.py | 206 ++++++ src/p3d/singleTask.py | 67 ++ src/p3d/wxPanda.py | 133 ++++ src/pandaEditor/__init__.py | 23 + src/pandaEditor/actions/__init__.py | 11 + src/pandaEditor/actions/add.py | 17 + src/pandaEditor/actions/base.py | 17 + src/pandaEditor/actions/composite.py | 19 + src/pandaEditor/actions/deselect.py | 19 + src/pandaEditor/actions/manager.py | 40 ++ src/pandaEditor/actions/parent.py | 21 + src/pandaEditor/actions/remove.py | 17 + src/pandaEditor/actions/select.py | 16 + src/pandaEditor/actions/setAttribute.py | 30 + src/pandaEditor/actions/transform.py | 22 + src/pandaEditor/app.py | 279 ++++++++ src/pandaEditor/commands.py | 82 +++ src/pandaEditor/editor/__init__.py | 7 + src/pandaEditor/editor/base.py | 13 + src/pandaEditor/editor/nodes/__init__.py | 7 + src/pandaEditor/editor/nodes/constants.py | 2 + src/pandaEditor/editor/nodes/lensNode.py | 16 + src/pandaEditor/editor/nodes/nodePath.py | 34 + src/pandaEditor/editor/plugins/__init__.py | 6 + src/pandaEditor/editor/plugins/base.py | 23 + src/pandaEditor/editor/plugins/manager.py | 31 + src/pandaEditor/editor/sceneParser.py | 109 ++++ src/pandaEditor/game/__init__.py | 4 + src/pandaEditor/game/base.py | 15 + src/pandaEditor/game/nodes/__init__.py | 3 + src/pandaEditor/game/nodes/actor.py | 30 + src/pandaEditor/game/nodes/ambientLight.py | 10 + src/pandaEditor/game/nodes/attributes.py | 64 ++ src/pandaEditor/game/nodes/base.py | 51 ++ src/pandaEditor/game/nodes/baseCam.py | 9 + src/pandaEditor/game/nodes/baseCamera.py | 9 + src/pandaEditor/game/nodes/camera.py | 10 + src/pandaEditor/game/nodes/collisionNode.py | 18 + src/pandaEditor/game/nodes/constants.py | 3 + .../game/nodes/directionalLight.py | 21 + src/pandaEditor/game/nodes/lensNode.py | 21 + src/pandaEditor/game/nodes/light.py | 25 + src/pandaEditor/game/nodes/manager.py | 74 +++ src/pandaEditor/game/nodes/modelNode.py | 10 + src/pandaEditor/game/nodes/modelRoot.py | 73 +++ src/pandaEditor/game/nodes/nodePath.py | 91 +++ src/pandaEditor/game/nodes/pandaNode.py | 10 + src/pandaEditor/game/nodes/pointLight.py | 20 + src/pandaEditor/game/nodes/spotlight.py | 23 + src/pandaEditor/game/plugins/__init__.py | 1 + src/pandaEditor/game/plugins/base.py | 22 + src/pandaEditor/game/plugins/manager.py | 69 ++ src/pandaEditor/game/sceneParser.py | 88 +++ src/pandaEditor/gizmos/__init__.py | 8 + src/pandaEditor/gizmos/axis.py | 130 ++++ src/pandaEditor/gizmos/base.py | 273 ++++++++ src/pandaEditor/gizmos/constants.py | 13 + src/pandaEditor/gizmos/manager.py | 104 +++ src/pandaEditor/gizmos/rotation.py | 219 +++++++ src/pandaEditor/gizmos/scale.py | 101 +++ src/pandaEditor/gizmos/translation.py | 99 +++ src/pandaEditor/project.py | 279 ++++++++ src/pandaEditor/scene.py | 124 ++++ src/pandaEditor/selection.py | 190 ++++++ src/pandaEditor/showBase.py | 174 +++++ src/pandaEditor/ui/__init__.py | 11 + src/pandaEditor/ui/baseDialog.py | 22 + src/pandaEditor/ui/consolePanel.py | 7 + src/pandaEditor/ui/customProperties.py | 123 ++++ src/pandaEditor/ui/document.py | 59 ++ src/pandaEditor/ui/lightLinkerPanel.py | 23 + src/pandaEditor/ui/mainFrame.py | 616 ++++++++++++++++++ src/pandaEditor/ui/projectSettingsPanel.py | 31 + src/pandaEditor/ui/propertiesPanel.py | 311 +++++++++ src/pandaEditor/ui/resourcesPanel.py | 134 ++++ src/pandaEditor/ui/sceneGraphBasePanel.py | 228 +++++++ src/pandaEditor/ui/sceneGraphPanel.py | 73 +++ src/pandaEditor/ui/viewport.py | 25 + src/utils/__init__.py | 5 + src/utils/directoryWatcher.py | 88 +++ src/utils/functions.py | 18 + src/utils/singleton.py | 14 + src/utils/wrappedFunction.py | 30 + src/wxExtra/__init__.py | 10 + src/wxExtra/actionItem.py | 36 + src/wxExtra/auiManagerConfig.py | 85 +++ src/wxExtra/compositeDropTarget.py | 43 ++ src/wxExtra/customAuiToolBar.py | 46 ++ src/wxExtra/customMenu.py | 58 ++ src/wxExtra/customTreeCtrl.py | 60 ++ src/wxExtra/dirTreeCtrl.py | 314 +++++++++ src/wxExtra/logPanel.py | 58 ++ src/wxExtra/propertyGrid.py | 423 ++++++++++++ src/wxExtra/utils.py | 95 +++ 118 files changed, 7948 insertions(+) create mode 100644 .gitignore create mode 100644 src/data/images/arrow-curve-flip.png create mode 100644 src/data/images/arrow-curve.png create mode 100644 src/data/images/disk-black-pencil.png create mode 100644 src/data/images/disk-black.png create mode 100644 src/data/images/document.png create mode 100644 src/data/images/folder-horizontal-open.png create mode 100644 src/main.py create mode 100644 src/p3d/__init__.py create mode 100644 src/p3d/camera.py create mode 100644 src/p3d/commonUtils.py create mode 100644 src/p3d/constants.py create mode 100644 src/p3d/displayShading.py create mode 100644 src/p3d/editorCamera.py create mode 100644 src/p3d/frameRate.py create mode 100644 src/p3d/functions.py create mode 100644 src/p3d/geometry.py create mode 100644 src/p3d/marquee.py create mode 100644 src/p3d/mouse.py create mode 100644 src/p3d/mousePicker.py create mode 100644 src/p3d/nodePathObject.py create mode 100644 src/p3d/object.py create mode 100644 src/p3d/pandaBehaviour.py create mode 100644 src/p3d/pandaManager.py create mode 100644 src/p3d/pandaObject.py create mode 100644 src/p3d/singleTask.py create mode 100644 src/p3d/wxPanda.py create mode 100644 src/pandaEditor/__init__.py create mode 100644 src/pandaEditor/actions/__init__.py create mode 100644 src/pandaEditor/actions/add.py create mode 100644 src/pandaEditor/actions/base.py create mode 100644 src/pandaEditor/actions/composite.py create mode 100644 src/pandaEditor/actions/deselect.py create mode 100644 src/pandaEditor/actions/manager.py create mode 100644 src/pandaEditor/actions/parent.py create mode 100644 src/pandaEditor/actions/remove.py create mode 100644 src/pandaEditor/actions/select.py create mode 100644 src/pandaEditor/actions/setAttribute.py create mode 100644 src/pandaEditor/actions/transform.py create mode 100644 src/pandaEditor/app.py create mode 100644 src/pandaEditor/commands.py create mode 100644 src/pandaEditor/editor/__init__.py create mode 100644 src/pandaEditor/editor/base.py create mode 100644 src/pandaEditor/editor/nodes/__init__.py create mode 100644 src/pandaEditor/editor/nodes/constants.py create mode 100644 src/pandaEditor/editor/nodes/lensNode.py create mode 100644 src/pandaEditor/editor/nodes/nodePath.py create mode 100644 src/pandaEditor/editor/plugins/__init__.py create mode 100644 src/pandaEditor/editor/plugins/base.py create mode 100644 src/pandaEditor/editor/plugins/manager.py create mode 100644 src/pandaEditor/editor/sceneParser.py create mode 100644 src/pandaEditor/game/__init__.py create mode 100644 src/pandaEditor/game/base.py create mode 100644 src/pandaEditor/game/nodes/__init__.py create mode 100644 src/pandaEditor/game/nodes/actor.py create mode 100644 src/pandaEditor/game/nodes/ambientLight.py create mode 100644 src/pandaEditor/game/nodes/attributes.py create mode 100644 src/pandaEditor/game/nodes/base.py create mode 100644 src/pandaEditor/game/nodes/baseCam.py create mode 100644 src/pandaEditor/game/nodes/baseCamera.py create mode 100644 src/pandaEditor/game/nodes/camera.py create mode 100644 src/pandaEditor/game/nodes/collisionNode.py create mode 100644 src/pandaEditor/game/nodes/constants.py create mode 100644 src/pandaEditor/game/nodes/directionalLight.py create mode 100644 src/pandaEditor/game/nodes/lensNode.py create mode 100644 src/pandaEditor/game/nodes/light.py create mode 100644 src/pandaEditor/game/nodes/manager.py create mode 100644 src/pandaEditor/game/nodes/modelNode.py create mode 100644 src/pandaEditor/game/nodes/modelRoot.py create mode 100644 src/pandaEditor/game/nodes/nodePath.py create mode 100644 src/pandaEditor/game/nodes/pandaNode.py create mode 100644 src/pandaEditor/game/nodes/pointLight.py create mode 100644 src/pandaEditor/game/nodes/spotlight.py create mode 100644 src/pandaEditor/game/plugins/__init__.py create mode 100644 src/pandaEditor/game/plugins/base.py create mode 100644 src/pandaEditor/game/plugins/manager.py create mode 100644 src/pandaEditor/game/sceneParser.py create mode 100644 src/pandaEditor/gizmos/__init__.py create mode 100644 src/pandaEditor/gizmos/axis.py create mode 100644 src/pandaEditor/gizmos/base.py create mode 100644 src/pandaEditor/gizmos/constants.py create mode 100644 src/pandaEditor/gizmos/manager.py create mode 100644 src/pandaEditor/gizmos/rotation.py create mode 100644 src/pandaEditor/gizmos/scale.py create mode 100644 src/pandaEditor/gizmos/translation.py create mode 100644 src/pandaEditor/project.py create mode 100644 src/pandaEditor/scene.py create mode 100644 src/pandaEditor/selection.py create mode 100644 src/pandaEditor/showBase.py create mode 100644 src/pandaEditor/ui/__init__.py create mode 100644 src/pandaEditor/ui/baseDialog.py create mode 100644 src/pandaEditor/ui/consolePanel.py create mode 100644 src/pandaEditor/ui/customProperties.py create mode 100644 src/pandaEditor/ui/document.py create mode 100644 src/pandaEditor/ui/lightLinkerPanel.py create mode 100644 src/pandaEditor/ui/mainFrame.py create mode 100644 src/pandaEditor/ui/projectSettingsPanel.py create mode 100644 src/pandaEditor/ui/propertiesPanel.py create mode 100644 src/pandaEditor/ui/resourcesPanel.py create mode 100644 src/pandaEditor/ui/sceneGraphBasePanel.py create mode 100644 src/pandaEditor/ui/sceneGraphPanel.py create mode 100644 src/pandaEditor/ui/viewport.py create mode 100644 src/utils/__init__.py create mode 100644 src/utils/directoryWatcher.py create mode 100644 src/utils/functions.py create mode 100644 src/utils/singleton.py create mode 100644 src/utils/wrappedFunction.py create mode 100644 src/wxExtra/__init__.py create mode 100644 src/wxExtra/actionItem.py create mode 100644 src/wxExtra/auiManagerConfig.py create mode 100644 src/wxExtra/compositeDropTarget.py create mode 100644 src/wxExtra/customAuiToolBar.py create mode 100644 src/wxExtra/customMenu.py create mode 100644 src/wxExtra/customTreeCtrl.py create mode 100644 src/wxExtra/dirTreeCtrl.py create mode 100644 src/wxExtra/logPanel.py create mode 100644 src/wxExtra/propertyGrid.py create mode 100644 src/wxExtra/utils.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7e99e36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc \ No newline at end of file diff --git a/src/data/images/arrow-curve-flip.png b/src/data/images/arrow-curve-flip.png new file mode 100644 index 0000000000000000000000000000000000000000..8ad24870b209fe8f11a247053eed8e081d232495 GIT binary patch literal 3666 zcmV-Y4z2NtP)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z000AhNkl=@}t zqtTi3opZi3-!LM=Q7#Kdyp9}z-S*N~upI}&aUg8FXIuzyAz*PH0Pu#EIX#U^p>RVj3bazeQ>RkC zRtjBf?Cd~Ed1zLHBgx)g92R`>blN8Zv{C>78HO-g|7Vf|C1tSi@T6ZXY8XRMQX*`% z{&nz!DhQO6!PE1{{X#)PN)!pri~I=V$yvAw(D& z0Dw{ki;o`n83Pz=yGo^1WhZQ|U9Oz`ZMF1FL(w?^Zvm(r4A2R_FhA?(^DRhu^>uG- z>|Q(93=o0|$FT-xrj8BVb^A!_!jD%{KjreX0A2);zZQTfkWvOuoW94uv{pp1TtnCj zA(eve^qg}JTL?H22XQZkSkwi#ILhS^p2x*~v*VLdm!JIlyY-g=ys)m z7dI`a^(yedZhVm= zzWi)Cn0d(eR}0M>nvQJ^tXEqYcWtCyfqcFN|L&V7ax0sU)$8GhUE-}a1(Z=zW`6kO zn`|OAjAAt2vsQc%b~1rTG68p_ZFVvN3?+k2+o=?v9L)4w~U%FAE{;T8Q*5 z$)RtTawh=1zV96nQAR6;{JS3nql=IGf9?Xi3@}o{vaC5G;!4U`tKJx{Z&omMV{-}5RZ>n~S1P(7rMKS;#-3a78ye6Z?Xew41K8=<1`#chF=u6? z>E(a^{lTr=>Y4i7^!-Z8!9&H4h%!d0O!vX0dT&{867UUvZMMD(d@ww}#>k$qoF z$@+<18$@eVUVF1A*IFqUt>Jp!fp@glWisYf0I|LRJrH96!0(=!FoeZ99M|0s&{o|) k7-PN9iT(fS_gjAt045>N-8${nPyhe`07*qoM6N<$f&g~K;s5{u literal 0 HcmV?d00001 diff --git a/src/data/images/arrow-curve.png b/src/data/images/arrow-curve.png new file mode 100644 index 0000000000000000000000000000000000000000..c14aff9529092a6ecdfe9c5ded9578868c8cf7b6 GIT binary patch literal 929 zcmV;S177@zP)1Nzpj8xHsRyAqA!yNq6r}Z7;@{!LlXw)mDq<;UPgQST#E(;@p)H~)imk;;6HA*k z-H+_<+jZVx@Vd!Dy3M2ooJblq&h@g)A&05Pc&elnxd2mSo4jZk2fUw zE0MA0pqENcMwVs6`Z|THLkSU|>yq$$;3ZM`6s`m4306s_uZ<*(2$KLsF~Kl67#%t) zKA8THuBAyWf-nM)`2-m5>5RmZ@o*%3!0Q)y;(W_W!`w72&~?m62PCmzWT;1+n0}WA zcW|Y81Z^1I&Hk?F;OUcxVz$Nc#i9w>RUH9ss5kVCfRSCf&?in#FYF?zCJ0jmKXWR6 z?s#u|$J$y6WSP1GTND&kgQBdU2H4|FXzIQrI4=Uujo0UegOMOyPW6kAp3av^cvn0N zIFCJctha5judA(Nd0BBHR!s`oEd>gyiX>L0F9^;HsS1r@X(p_!=0PDgHi6EDu~H)J zf@C1z8x~I-j^%PCP<0EIaKm@a0BR$kH}FSY*4gTZ*UvH;1eb`wOVtFau4Ft$sfW+X z2F&OLiX6e|^@ERaAYinF+YT0^Ww~tWHppg0l+G{@^N*i9;JhI078%cuf#i{BWO>Vm zj6Vn=ziCD`He~f%Zc{51HMywjrdl$oZcuyseD@7s5;sdOK_bAxmxoWbBNu*eQ_LX2 z2Lt|31zj-~-e$iptt`@#F^3!8;OiyyRrC#ZEa87Bhc#w=dFD30UDSD7h1p09I+ zKLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z0008gNklG<$x{ zX4S%K#A$+{RzZ}T!Zry8X&bR*ifT~~ek6uPl;p_!xTl5lp7Z&fFb^DfoO|#2-TyuJ z<1r$_L7D^r&iQo!#{r;cXz0a$5Av#K@q`Hs;IB7t>Ml5JOWKkNDdDdv@0S^O-+3= zO_Kp&0CoXH1R{c60`Oz~CFbVlaJsh_<4U1=aIm0{O@z+Q&SSJwSmxFM09Q&uWja~5 zowKgLH(p_JVHD@vPavL1p(`9lRYOBTKQIO-k286hufS3YX4b&XjTtN@Z=$WW8I#Xn zBNV!ZBlYz;eO%)lPM(qqEFu_|1pxT-eHJV4Z=<8F1UF?pI`9x-7SRLPvYt14C-30peoo^eCE!c&*bm~SVS;w8;frr<6QR`9zDE>VDlwZ z)|~Q84gea_-T;-^5#2pEFgo%Pt!D>NS0)VDz zB?0ad2_aw@HVnf?AP|7x??*bFc4LkSVM7R!1JHH7V2L;Y5RvZl`9MVAoWn2_3}Z)) zrfE5`Y%iw3x$KBY&wdZ?#pChM6%`dZ3WN}lQs%_6Jzqgy4gA@F7=Sy8L}FFfbr5k# zse^M4DJ3}PZp?8aUbM(QpU(jwa^Hch{X76*KuYE&-6-T?&3fbbtaL z_(&x{1$hYyFn2G32)10FCbB5_n$5^@wVI0n=k6tV7z_@sKdA(N(NYPf(<#YXVJW3C zTdh`0B+xSuU5U1cqVAu^Z9sx}qd3bt}! zs00?^c03+`qfFmtFj$u>#7Rt+;k7LUz9%IaBM}fEMfZTRpKlN_@G;9dfIIvuZ4;gmne+_-Hv>v*^*kmDVT{EDIoB-&f2lMO)U%jZU;AA})9s4hf{43j%Qm`o0aBu|Ib)h>LsMG68 zrS~)%-Q)5hAD}CYk`hxO^;!*l-v>Vkz_xAp7>~d66@=k01z8XZ7Nrt6jsxfAvWn%k z$WV}{1oYe}HWv`a3iP145b_OK3J8GCWO%W&AKLZ*U+5Lu!Sk^o_Z5E4Meg@_7P6crJiNL9pw)e1;Xm069{HJUZAPk55R%$-RIA z6-eL&AQ0xu!e<4=008gy@A0LT~suv4>S3ILP<0Bm`DLLvaF4FK%)Nj?Pt*r}7;7Xa9z9H|HZjR63e zC`Tj$K)V27Re@400>HumpsYY5E(E}?0f1SyGDiY{y#)Yvj#!WnKwtoXnL;eg03bL5 z07D)V%>y7z1E4U{zu>7~aD})?0RX_umCct+(lZpemCzb@^6=o|A>zVpu|i=NDG+7} zl4`aK{0#b-!z=TL9Wt0BGO&T{GJWpjryhdijfaIQ&2!o}p04JRKYg3k&Tf zVxhe-O!X z{f;To;xw^bEES6JSc$k$B2CA6xl)ltA<32E66t?3@gJ7`36pmX0IY^jz)rRYwaaY4 ze(nJRiw;=Qb^t(r^DT@T3y}a2XEZW-_W%Hszxj_qD**t_m!#tW0KDiJT&R>6OvVTR z07RgHDzHHZ48atvzz&?j9lXF70$~P3Knx_nJP<+#`N z#-MZ2bTkiLfR>_b(HgWKJ%F~Nr_oF3b#wrIijHG|(J>BYjM-sajE6;FiC7vY#};Gd zST$CUHDeuEH+B^pz@B062qXfFfD`NpUW5?BY=V%GM_5c)L#QR}BeW8_2v-S%gfYS= zB9o|3v?Y2H`NVi)In3rTB8+ej^> zQ=~r95NVuDChL%G$=>7$vVg20myx%S50Foi`^m%Pw-h?Xh~i8Mq9jtJloCocWk2Nv zrJpiFnV_ms&8eQ$2&#xWpIS+6pmtC%Q-`S&GF4Q#^mhymh7E(qNMa}%YZ-ePrx>>xFPTiH1=E+A$W$=bG8>s^ zm=Bn5Rah$aDtr}@$`X}2l~$F0mFKEdRdZE8)p@E5RI61Ft6o-prbbn>P~)iy)E2AN zsU20jsWz_8Qg>31P|s0cqrPALg8E|(vWA65poU1JRAaZs8I2(p#xiB`SVGovRs-uS zYnV-9TeA7=Om+qP8+I>yOjAR1s%ETak!GFdam@h^# z)@rS0t$wXH+Irf)+G6c;?H29p+V6F6oj{!|o%K3xI`?%6x;DB|x`n#ibhIR?(H}Q3Gzd138Ei2)WAMz7W9Vy`X}HnwgyEn!VS)>mv$8&{hQn>w4zwy3R}t;BYlZQm5)6pty=DfLrs+A-|>>;~;Q z_F?uV_HFjh9n2gO9o9Q^JA86v({H5aB!kjoO6 zc9$1ZZKsN-Zl8L~mE{`ly3)1N^`o1+o7}D0ZPeY&J;i;i`%NyJ8_8Y6J?}yE@b_5a zam?eLr<8@mESk|3$_SkmS{wQ>%qC18))9_|&j{ZT zes8AvOzF(F2#DZEY>2oYX&IRp`F#{ADl)1r>QS^)ba8a|EY_^#S^HO&t^Rgqwv=MZThqqEWH8 zxJo>d=ABlR_Bh=;eM9Tw|Ih34~oTE|= zX_mAr*D$vzw@+p(E0Yc6dFE}(8oqt`+R{gE3x4zjX+Sb3_cYE^= zgB=w+-tUy`ytONMS8KgRef4hA?t0j zufM;t32jm~jUGrkaOInTZ`zyfns>EuS}G30LFK_G-==(f<51|K&cocp&EJ`SxAh3? zNO>#LI=^+SEu(FqJ)ynt=!~PC9bO$rzPJB=?=j6w@a-(u02P7 zaQ)#(uUl{HW%tYNS3ItC^iAtK(eKlL`f9+{bJzISE?u8_z3;~C8@FyI-5j_jy7l;W z_U#vU3hqqYU3!mrul&B+{ptt$59)uk{;_4iZQ%G|z+lhASr6|H35TBkl>gI*;nGLU zN7W-nBaM%pA0HbH8olyl&XeJ%vZoWz%6?Y=dFykl=imL}`%BMQ{Mhgd`HRoLu6e2R za__6DuR6yg#~-}Tc|Gx_{H@O0eebyMy5GmWADJlpK>kqk(fVV@r_fLLKIeS?{4e)} z^ZO;zpECde03c&XQcVB=dL;k=fP(-4`Tqa_faw4Lbua(`>RI+y?e7jKeZ#YO-C z0&Gb{K~#9!?3KT36hRcnKeKzkNDyNohd+pjf}}|&25bev#zK1wK`XTpMJ0tz0_iL* z`~w6P8w>431QEm-V~mN3IbzIjbDY_`+ufV@EOz(2J2jHibiQV0cISP+?|tXZ8>6ah zr8#V|Z8<>-;N_tCb0h|gp%*yyn=SWM84-m>)l-`YrdO5I2agZ+gF?&EeSC5?iE`wp z41lx1{*BN@q37hS8=XBDCm9GJi6=vhk-kZpHWp_448qw zOAoJMEviaXS&E84ytdT}5`-uMBGJBNR7F*Aoiw8lE)D|Mn=%l=!CE8;V!+4|6;z@% zs+g1m0%{^BFd$Hu%!#pLbKQhMaU#H9gGZ6JjMDO71XK`>2(oD~CN5Je5y9VpCvz(} z*;G_@KO|Bdh_$pe=Ej~8cq`=6am^*?lIRUh-;%lGx@6KEH4Zo9A{$S|8MsM`erbtp z2D*=&Y3pL`J9fSg=<}AQKE1npWvEVoDk@63I8U}~4>iw2{A!#1!C`s)`E<=Pk!n2y zueeBl_aSVxj9A+izMILgJUu+?hm18z6qHK{4z8OeD3{vqKcT-~T%9k>I);S?iG=my z61i=8YSl8f7PNKP=gBwoe#oe)6IfhC zs8lNCvz=7dOU*5DGbYwAOnvz3!25azo==CWkn7lv^?jtx=mA#d3X5J<-U4AGLFbp5 ziM3sO5A3K_Dx}>^+xizK$JT<7SElhMS3~aJyFHZq7cZDG3|rxGQ@j5E=x=1-2LLHg V) target_list[0].lower(): + raise OSError, ''.join( ['Target is on a different drive to base. Target: ', target_list[0], ', base: ', base_list[0]] ) + + # Starting from the filepath root, work out how much of the filepath is + # shared by base and target + for i in range( min( len( base_list ), len( target_list ) ) ): + if base_list[i].lower() <> target_list[i].lower(): + break + else: + + # If we broke out of the loop, i is pointing to the first differing + # path elements. If we didn't break out of the loop, i is pointing to + # identical path elements. Increment i so that in all cases it points + # to the first differing path elements. + i += 1 + + rel_list = [os.pardir] * ( len( base_list ) - i ) + target_list[i:] + return os.path.join( *rel_list ) + + +def GetTrsMatrices( xform ): + """ + Return translation, rotation and scale matrices back for the specified + transform. + """ + # Get translation and rotation matrices + rotMat = pm.Mat4() + xform.getQuat().extractToMatrix( rotMat ) + transMat = pm.Mat4().translateMat( xform.getPos() ) + + # More care must be taken to get the scale matrix as simply calling + # Mat4().scaleMat( xform.getScale() ) won't account for shearing or other + # weird scaling. To get this matrix simply remove the translation and + # rotation components from the xform. + invRotMat = pm.Mat4() + invRotMat.invertFrom( rotMat ) + invTransMat = pm.Mat4() + invTransMat.invertFrom( transMat ) + scaleMat = xform.getMat() * invTransMat * invRotMat + + return transMat, rotMat, scaleMat + + +def GetInvertedMatrix( mat ): + """ + Invert the indicated matrix, sending back a new matrix. + """ + invMat = pm.Mat4() + invMat.invertFrom( mat ) + return invMat \ No newline at end of file diff --git a/src/p3d/constants.py b/src/p3d/constants.py new file mode 100644 index 0000000..f64b334 --- /dev/null +++ b/src/p3d/constants.py @@ -0,0 +1,6 @@ +import pandac.PandaModules as pm + + +X_AXIS = pm.Vec3(1, 0, 0) +Y_AXIS = pm.Vec3(0, 1, 0) +Z_AXIS = pm.Vec3(0, 0, 1) \ No newline at end of file diff --git a/src/p3d/displayShading.py b/src/p3d/displayShading.py new file mode 100644 index 0000000..9cf9553 --- /dev/null +++ b/src/p3d/displayShading.py @@ -0,0 +1,30 @@ +from direct.showbase.DirectObject import DirectObject + + +class DisplayShading( DirectObject ): + + """Toggles display shading.""" + + def SetWireframe( self, value ): + if value: + base.wireframeOn() + else: + base.wireframeOff() + + def SetTexture( self, value ): + if value: + base.textureOn() + else: + base.textureOff() + + def Wireframe( self ): + self.SetWireframe( True ) + self.SetTexture( False ) + + def Shade( self ): + self.SetWireframe( False ) + self.SetTexture( False ) + + def Texture( self ): + self.SetWireframe( False ) + self.SetTexture( True ) \ No newline at end of file diff --git a/src/p3d/editorCamera.py b/src/p3d/editorCamera.py new file mode 100644 index 0000000..2b6f2b8 --- /dev/null +++ b/src/p3d/editorCamera.py @@ -0,0 +1,43 @@ +from pandac.PandaModules import Vec2, Vec3 + +import p3d + + +class EditorCamera( p3d.Camera ): + + """Base editor camera class.""" + + def __init__( self, *args, **kwargs ): + self.speed = kwargs.pop( 'speed', 1 ) + kwargs['pos'] = kwargs.pop( 'pos', (-250, -250, 200) ) + kwargs['style'] = kwargs.pop( 'style', p3d.CAM_USE_DEFAULT | + p3d.CAM_VIEWPORT_AXES ) + p3d.Camera.__init__( self, *args, **kwargs ) + + # Create mouse + base.disableMouse() + self.mouse = p3d.Mouse( 'mouse', *args, **kwargs ) + self.mouse.Start() + + def OnUpdate( self, task ): + """ + Task to control mouse events. Gets called every frame and will update + the scene accordingly. + """ + p3d.Camera.OnUpdate( self, task ) + + # Return if no mouse is found or alt not down + if not self.mouseWatcherNode.hasMouse() or not p3d.MOUSE_ALT in self.mouse.modifiers: + return + + # ORBIT - If left mouse down + if self.mouse.buttons[0]: + self.Orbit( Vec2(self.mouse.dx * self.speed, self.mouse.dy * self.speed) ) + + # DOLLY - If middle mouse down + elif self.mouse.buttons[1]: + self.Move( Vec3(self.mouse.dx * self.speed, 0, -self.mouse.dy * self.speed) ) + + # ZOOM - If right mouse down + elif self.mouse.buttons[2]: + self.Move( Vec3(0, -self.mouse.dx * self.speed, 0) ) \ No newline at end of file diff --git a/src/p3d/frameRate.py b/src/p3d/frameRate.py new file mode 100644 index 0000000..f028b9b --- /dev/null +++ b/src/p3d/frameRate.py @@ -0,0 +1,15 @@ +import sys +from direct.showbase.DirectObject import DirectObject + + +class FrameRate( DirectObject ): + + """Toggles displaying the framerate with F12.""" + + def __init__( self ): + self.state = False + self.accept( 'f12', self.Toggle ) + + def Toggle( self ): + self.state = not self.state + getBase().setFrameRateMeter( self.state ) \ No newline at end of file diff --git a/src/p3d/functions.py b/src/p3d/functions.py new file mode 100644 index 0000000..df0cf63 --- /dev/null +++ b/src/p3d/functions.py @@ -0,0 +1,27 @@ +import pandac.PandaModules as pm + + +def Str2Bool( string ): + if string.lower() == 'true': + return True + return False + + +def FloatTuple2Str( flts ): + return ' '.join( [str( flt ) for flt in flts] ) + + +def Str2FloatTuple( string ): + buffer = string.split( ' ' ) + return tuple( [float( elem ) for elem in buffer] ) + + +def Mat42Str( mat ): + buffer = [FloatTuple2Str( mat.getRow( i ) ) for i in range( 4 )] + return ' '.join( buffer ) + + +def Str2Mat4( string ): + mat = pm.Mat4() + mat.set( *Str2FloatTuple( string ) ) + return mat \ No newline at end of file diff --git a/src/p3d/geometry.py b/src/p3d/geometry.py new file mode 100644 index 0000000..39a0531 --- /dev/null +++ b/src/p3d/geometry.py @@ -0,0 +1,333 @@ +import math + +import pandac.PandaModules as pm + + +def GetPointsForSquare( x, y, reverse=False ): + points = [] + + points.append( (0, 0) ) + points.append( (0, y) ) + points.append( (x, y) ) + points.append( (x, 0) ) + + # Reverse the order if necessary + if reverse: + points.reverse() + + return points + + +def GetPointsForSquare2( x, y, reverse=False ): + points = [] + + points.append( pm.Point2(0, 0) ) + points.append( pm.Point2(0, y) ) + points.append( pm.Point2(x, y) ) + points.append( pm.Point2(x, 0) ) + + # Reverse the order if necessary + if reverse: + points.reverse() + + return points + + +def GetPointsForBox( x, y, z ): + points = [] + + for dx in (0, x): + for p in GetPointsForSquare( y, z, dx ): + points.append( (dx, p[0], p[1] ) ) + + for dy in (0, y): + for p in GetPointsForSquare( x, z, not dy ): + points.append( (p[0], dy, p[1] ) ) + + for dz in (0, z): + for p in GetPointsForSquare( x, y, dz ): + points.append( (p[0], p[1], dz ) ) + + return points + + +def GetPointsForArc( degrees, numSegs, reverse=False ): + points = [] + + radians = math.radians( degrees ) + for i in range( numSegs + 1 ): + a = radians * i / numSegs + y = math.sin( a ) + x = math.cos( a ) + + points.append( (x, y) ) + + # Reverse the order if necessary + if reverse: + points.reverse() + + return points + + +def RotatePoint3( p, v1, v2 ): + v1 = pm.Vec3( v1 ) + v2 = pm.Vec3( v2 ) + v1.normalize() + v2.normalize() + cross = v1.cross( v2 ) + cross.normalize() + if cross.length(): + a = v1.angleDeg( v2 ) + quat = pm.Quat() + quat.setFromAxisAngle( a, cross ) + p = quat.xform( p ) + + return p + + +def GetGeomTriangle( v1, v2, v3 ): + tri = pm.GeomTriangles( pm.Geom.UHDynamic ) + tri.addVertex( v1 ) + tri.addVertex( v2 ) + tri.addVertex( v3 ) + tri.closePrimitive() + + return tri + + +class Arc( pm.NodePath ): + + """NodePath class representing a wire arc.""" + + def __init__( self, radius=1.0, numSegs=16, degrees=360, axis=pm.Vec3(1 , 0, 0), + thickness=1.0, origin=pm.Point3(0, 0, 0) ): + + # Create line segments + self.ls = pm.LineSegs() + self.ls.setThickness( thickness ) + + # Get the points for an arc + for p in GetPointsForArc( degrees, numSegs ): + + # Draw the point rotated around the desired axis + p = pm.Point3(p[0], p[1], 0) - origin + p = RotatePoint3( p, pm.Vec3(0, 0, 1), pm.Vec3( axis ) ) + self.ls.drawTo( p * radius ) + + # Init the node path, wrapping the lines + node = self.ls.create() + pm.NodePath.__init__( self, node ) + + +def Circle( radius=1.0, numSegs=16, axis=pm.Vec3(1 , 0, 0), + thickness=1.0, origin=pm.Point3(0, 0, 0) ): + + # Create line segments + ls = pm.LineSegs() + ls.setThickness( thickness ) + + # Get the points for an arc + for p in GetPointsForArc( 360, numSegs ): + + # Draw the point rotated around the desired axis + p = pm.Point3(p[0], p[1], 0) - origin + p = RotatePoint3( p, pm.Vec3(0, 0, 1), pm.Vec3( axis ) ) + ls.drawTo( p * radius ) + + return ls.create() + + +def Square( width=1, height=1, axis=pm.Vec3(1, 0, 0), thickness=1.0, origin=pm.Point3(0, 0, 0) ): + """Return a geom node representing a wire square.""" + # Create line segments + ls = pm.LineSegs() + ls.setThickness( thickness ) + + # Get the points for a square + points = GetPointsForSquare( width, height ) + points.append( points[0] ) + for p in points: + + # Draw the point rotated around the desired axis + p = pm.Point3(p[0], p[1], 0) - origin + p = RotatePoint3( p, pm.Vec3(0, 0, 1), axis ) + ls.drawTo( p ) + + # Return the geom node + return ls.create() + + +def Cone( radius=1.0, height=1.0, numSegs=16, degrees=360, + axis=pm.Vec3(0, 0, 1), origin=pm.Point3(0, 0, 0) ): + """Return a geom node representing a cone.""" + # Create vetex data format + gvf = pm.GeomVertexFormat.getV3n3() + gvd = pm.GeomVertexData( 'vertexData', gvf, pm.Geom.UHStatic ) + + # Create vetex writers for each type of data we are going to store + gvwV = pm.GeomVertexWriter( gvd, 'vertex' ) + gvwN = pm.GeomVertexWriter( gvd, 'normal' ) + + # Get the points for an arc + points = GetPointsForArc( degrees, numSegs, True ) + for i in range( len( points ) - 1 ): + + # Rotate the points around the desired axis + p1 = pm.Point3(points[i][0], points[i][1], 0) * radius + p1 = RotatePoint3( p1, pm.Vec3(0, 0, 1), axis ) - origin + p2 = pm.Point3(points[i + 1][0], points[i + 1][1], 0) * radius + p2 = RotatePoint3( p2, pm.Vec3(0, 0, 1), axis ) - origin + + cross = ( p2 - axis ).cross( p1 - axis ) + cross.normalize() + + gvwV.addData3f( p1 ) + gvwV.addData3f( axis * height - origin ) + gvwV.addData3f( p2 ) + gvwN.addData3f( cross ) + gvwN.addData3f( cross ) + gvwN.addData3f( cross ) + + # Base + gvwV.addData3f( p2 ) + gvwV.addData3f( pm.Point3(0, 0, 0) - origin ) + gvwV.addData3f( p1 ) + gvwN.addData3f( -axis ) + gvwN.addData3f( -axis ) + gvwN.addData3f( -axis ) + + geom = pm.Geom( gvd ) + for i in range( 0, gvwV.getWriteRow(), 3 ): + + # Create and add triangle + geom.addPrimitive( GetGeomTriangle( i, i + 1, i + 2 ) ) + + # Init the node path, wrapping the box + geomNode = pm.GeomNode( 'cone' ) + geomNode.addGeom( geom ) + return geomNode + + +class Box( pm.NodePath ): + + """NodePath class representing a polygonal box.""" + + def __init__( self, width=1, depth=1, height=1, origin=pm.Point3(0, 0, 0) ): + + # Create vetex data format + gvf = pm.GeomVertexFormat.getV3n3() + gvd = pm.GeomVertexData( 'vertexData', gvf, pm.Geom.UHStatic ) + + # Create vetex writers for each type of data we are going to store + gvwV = pm.GeomVertexWriter( gvd, 'vertex' ) + gvwN = pm.GeomVertexWriter( gvd, 'normal' ) + + # Write out all points + for p in GetPointsForBox( width, depth, height ): + gvwV.addData3f( pm.Point3( p ) - origin ) + + # Write out all the normals + for n in ( (-1, 0, 0), (1, 0, 0), (0, -1, 0), (0, 1, 0), (0, 0, -1), (0, 0, 1) ): + for i in range( 4 ): + gvwN.addData3f( n ) + + geom = pm.Geom( gvd ) + for i in range( 0, gvwV.getWriteRow(), 4 ): + + # Create and add both triangles + geom.addPrimitive( GetGeomTriangle( i, i + 1, i + 2 ) ) + geom.addPrimitive( GetGeomTriangle( i, i + 2, i + 3 ) ) + + # Init the node path, wrapping the box + geomNode = pm.GeomNode( 'box' ) + geomNode.addGeom( geom ) + pm.NodePath.__init__( self, geomNode ) + + +class Polygon( pm.NodePath ): + + def __init__( self, points, normals=None, texcoords=None, origin=pm.Point3(0, 0, 0) ): + + # Create vetex data format + gvf = pm.GeomVertexFormat.getV3n3t2() + gvd = pm.GeomVertexData( 'vertexData', gvf, pm.Geom.UHStatic ) + + # Create vetex writers for each type of data we are going to store + gvwV = pm.GeomVertexWriter( gvd, 'vertex' ) + gvwN = pm.GeomVertexWriter( gvd, 'normal' ) + gvwT = pm.GeomVertexWriter( gvd, 'texcoord' ) + + # Write out all points and normals + for i, point in enumerate( points ): + p = pm.Point3( point ) - origin + gvwV.addData3f( p ) + + # Calculate tex coords if none specified + if texcoords is None: + gvwT.addData2f( p.x / 10.0, p.y / 10.0 ) + else: + gvwT.addData2f( texcoords[i] ) + + # Calculate normals if none specified + if normals is None: + prevPoint = ( points[-1] if i == 0 else points[i-1] ) + nextPoint = ( points[0] if i == len( points ) - 1 else points[i+1] ) + cross = ( nextPoint - point ).cross( prevPoint - point ) + cross.normalize() + gvwN.addData3f( cross ) + else: + gvwN.addData3f( normals[i] ) + + geom = pm.Geom( gvd ) + for i in range( len( points ) - 1 ): + + # Create and add both triangles + geom.addPrimitive( GetGeomTriangle( 0, i, i + 1 ) ) + + # Init the node path, wrapping the polygon + geomNode = pm.GeomNode( 'poly' ) + geomNode.addGeom( geom ) + pm.NodePath.__init__( self, geomNode ) + + +def Line( start, end, thickness=1.0 ): + + """Return a geom node representing a simple line.""" + + # Create line segments + ls = pm.LineSegs() + ls.setThickness( thickness ) + ls.drawTo( pm.Point3( start ) ) + ls.drawTo( pm.Point3( end ) ) + + # Return the geom node + return ls.create() + + +def QuadWireframe( egg ): + + def RecursePoly( node, geo ): + + if isinstance( node, pm.EggPolygon ): + + # Get each vert position + poss = [] + for vert in node.getVertices(): + pos3 = vert.getPos3() + pos = pm.Point3( pos3.getX(), pos3.getY(),pos3.getZ() ) + poss.append( pos ) + + # Build lines + geo.combineWith( Line( poss[0], poss[1] ) ) + geo.combineWith( Line( poss[1], poss[2] ) ) + geo.combineWith( Line( poss[2], poss[3] ) ) + geo.combineWith( Line( poss[3], poss[0] ) ) + + # Recurse down hierarchy + if hasattr( node, 'getChildren' ): + for child in node.getChildren(): + RecursePoly( child, geo ) + + return geo + + return RecursePoly( egg, pm.GeomNode( 'quadWireframe' ) ) \ No newline at end of file diff --git a/src/p3d/marquee.py b/src/p3d/marquee.py new file mode 100644 index 0000000..88a29cf --- /dev/null +++ b/src/p3d/marquee.py @@ -0,0 +1,79 @@ +from pandac.PandaModules import NodePath, CardMaker, LineSegs, Point2 + +import p3d + + +TOLERANCE = 1e-3 + + +class Marquee( NodePath, p3d.SingleTask ): + + """Class representing a 2D marquee drawn by the mouse.""" + + def __init__( self, *args, **kwargs ): + colour = kwargs.pop( 'colour', (1, 1, 1, .2) ) + p3d.SingleTask.__init__( self, *args, **kwargs ) + + # Create a card maker + cm = CardMaker( self.name ) + cm.setFrame( 0, 1, 0, 1 ) + + # Init the node path, wrapping the card maker to make a rectangle + NodePath.__init__( self, cm.generate() ) + self.setColor( colour ) + self.setTransparency( 1 ) + self.reparentTo( self.root2d ) + self.hide() + + # Create the rectangle border + ls = LineSegs() + ls.moveTo( 0, 0, 0 ) + ls.drawTo( 1, 0, 0 ) + ls.drawTo( 1, 0, 1 ) + ls.drawTo( 0, 0, 1 ) + ls.drawTo( 0, 0, 0 ) + + # Attach border to rectangle + self.attachNewNode( ls.create() ) + + def OnUpdate( self, task ): + """ + Called every frame to keep the marquee scaled to fit the region marked + by the mouse's initial position and the current mouse position. + """ + # Check for mouse first, in case the mouse is outside the Panda window + if self.mouseWatcherNode.hasMouse(): + + # Get the other marquee point and scale to fit + pos = self.mouseWatcherNode.getMouse() - self.initMousePos + self.setScale( pos[0] if pos[0] else TOLERANCE, 1, pos[1] if pos[1] else TOLERANCE ) + + def OnStart( self ): + + # Move the marquee to the mouse position and show it + self.initMousePos = Point2( self.mouseWatcherNode.getMouse() ) + self.setPos( self.initMousePos[0], 1, self.initMousePos[1] ) + self.show() + + def OnStop( self ): + + # Hide the marquee + self.hide() + + def IsNodePathInside( self, np ): + """Test if the specified node path lies within the marquee area.""" + npWorldPos = np.getPos( self.rootNp ) + p3 = self.camera.getRelativePoint( self.rootNp, npWorldPos ) + + # Convert it through the lens to render2d coordinates + p2 = Point2() + if not self.camera.GetLens().project( p3, p2 ): + return False + + # Test point is within bounds of the marquee + min, max = self.getTightBounds() + if ( p2.getX() > min.getX() and p2.getX() < max.getX() and + p2.getY() > min.getZ() and p2.getY() < max.getZ() ): + return True + + return False \ No newline at end of file diff --git a/src/p3d/mouse.py b/src/p3d/mouse.py new file mode 100644 index 0000000..7139d1d --- /dev/null +++ b/src/p3d/mouse.py @@ -0,0 +1,63 @@ +from pandac.PandaModules import Vec2 + +import p3d + + +MOUSE_ALT = 0 + + +class Mouse( p3d.SingleTask ): + + """Class representing the mouse.""" + + def __init__( self, *args, **kwargs ): + p3d.SingleTask.__init__( self, *args, **kwargs ) + + self.x = 0 + self.y = 0 + self.dx = 0 + self.dy = 0 + self.buttons = [False, False, False] + self.modifiers = [] + + # Bind button events + self.accept( 'alt', self.SetModifier, [MOUSE_ALT] ) + self.accept( 'alt-up', self.ClearModifier, [MOUSE_ALT] ) + + self.accept( 'alt-mouse1', self.SetButton, [0, True] ) + self.accept( 'mouse1', self.SetButton, [0, True] ) + self.accept( 'mouse1-up', self.SetButton, [0, False] ) + + self.accept( 'alt-mouse2', self.SetButton, [1, True] ) + self.accept( 'mouse2', self.SetButton, [1, True] ) + self.accept( 'mouse2-up', self.SetButton, [1, False] ) + + self.accept( 'alt-mouse3', self.SetButton, [2, True] ) + self.accept( 'mouse3', self.SetButton, [2, True] ) + self.accept( 'mouse3-up', self.SetButton, [2, False] ) + + def OnUpdate( self, task ): + + # Get pointer from screen, calculate delta + mp = self.win.getPointer( 0 ) + self.dx = self.x - mp.getX() + self.dy = self.y - mp.getY() + self.x = mp.getX() + self.y = mp.getY() + + def SetModifier( self, modifier ): + + # Record modifier + if modifier not in self.modifiers: + self.modifiers.append( modifier ) + + def ClearModifier( self, modifier ): + + # Remove modifier + if modifier in self.modifiers: + self.modifiers.remove( modifier ) + + def SetButton( self, id, value ): + + # Record button value + self.buttons[id] = value \ No newline at end of file diff --git a/src/p3d/mousePicker.py b/src/p3d/mousePicker.py new file mode 100644 index 0000000..ad243af --- /dev/null +++ b/src/p3d/mousePicker.py @@ -0,0 +1,98 @@ +from pandac.PandaModules import CollisionTraverser, CollisionHandlerQueue, BitMask32 +from pandac.PandaModules import CollisionNode, CollisionRay + +import p3d + + +class MousePicker( p3d.SingleTask ): + + """ + Class to represent a ray fired from the input camera lens using the mouse. + """ + + def __init__( self, *args, **kwargs ): + p3d.SingleTask.__init__( self, *args, **kwargs ) + + self.fromCollideMask = kwargs.pop( 'fromCollideMask', None ) + + self.node = None + self.collEntry = None + + # Create collision nodes + self.collTrav = CollisionTraverser() + #self.collTrav.showCollisions( render ) + self.collHandler = CollisionHandlerQueue() + self.pickerRay = CollisionRay() + + # Create collision ray + pickerNode = CollisionNode( self.name ) + pickerNode.addSolid( self.pickerRay ) + pickerNode.setIntoCollideMask( BitMask32.allOff() ) + pickerNp = self.camera.attachNewNode( pickerNode ) + self.collTrav.addCollider( pickerNp, self.collHandler ) + + # Create collision mask for the ray if one is specified + if self.fromCollideMask is not None: + pickerNode.setFromCollideMask( self.fromCollideMask ) + + # Bind mouse button events + eventNames = ['mouse1', 'control-mouse1', 'mouse1-up'] + for eventName in eventNames: + self.accept( eventName, self.FireEvent, [eventName] ) + + def OnUpdate( self, task ): + + # Update the ray's position + if self.mouseWatcherNode.hasMouse(): + mp = self.mouseWatcherNode.getMouse() + self.pickerRay.setFromLens( self.camera.node(), mp.getX(), mp.getY() ) + + # Traverse the hierarchy and find collisions + self.collTrav.traverse( self.rootNp ) + if self.collHandler.getNumEntries(): + + # If we have hit something, sort the hits so that the closest is first + self.collHandler.sortEntries() + collEntry = self.collHandler.getEntry( 0 ) + node = collEntry.getIntoNode() + + # If this node is different to the last node, send a mouse leave + # event to the last node, and a mouse enter to the new node + if node != self.node: + if self.node is not None: + messenger.send( '%s-mouse-leave' % self.node.getName(), [self.collEntry] ) + messenger.send( '%s-mouse-enter' % node.getName(), [collEntry] ) + + # Send a message containing the node name and the event over name, + # including the collision entry as arguments + messenger.send( '%s-mouse-over' % node.getName(), [collEntry] ) + + # Keep these values + self.collEntry = collEntry + self.node = node + + elif self.node is not None: + + # No collisions, clear the node and send a mouse leave to the last + # node that stored + messenger.send( '%s-mouse-leave' % self.node.getName(), [self.collEntry] ) + self.node = None + + def FireEvent( self, event ): + """ + Send a message containing the node name and the event name, including + the collision entry as arguments. + """ + if self.node is not None: + messenger.send( '%s-%s' % ( self.node.getName(), event ), [self.collEntry] ) + + def GetFirstNodePath( self ): + """ + Return the first node in the collision queue if there is one, None + otherwise. + """ + if self.collHandler.getNumEntries(): + collEntry = self.collHandler.getEntry( 0 ) + return collEntry.getIntoNodePath() + + return None \ No newline at end of file diff --git a/src/p3d/nodePathObject.py b/src/p3d/nodePathObject.py new file mode 100644 index 0000000..e4d1211 --- /dev/null +++ b/src/p3d/nodePathObject.py @@ -0,0 +1,26 @@ +TAG_PANDA_OBJECT = 'PandaObject' + + +class NodePathObject( object ): + + """ + Basic building block class, designed to be attached to a node path in the + scene graph. Doing this creates a circular reference, so care must be + taken to clear the python tag on this class' node path before trying to + remove it. + """ + + def __init__( self, np ): + self.np = np + self.np.setPythonTag( TAG_PANDA_OBJECT, self ) + + def __del__( self ): + print TAG_PANDA_OBJECT, ' : ', self.np.getName(), ' DELETED' + + @staticmethod + def Get( np ): + return np.getPythonTag( TAG_PANDA_OBJECT ) + + @staticmethod + def Break( np ): + np.clearPythonTag( TAG_PANDA_OBJECT ) \ No newline at end of file diff --git a/src/p3d/object.py b/src/p3d/object.py new file mode 100644 index 0000000..e46ddd7 --- /dev/null +++ b/src/p3d/object.py @@ -0,0 +1,31 @@ +from direct.showbase.DirectObject import DirectObject + + +class Object( DirectObject ): + + def __init__( self, *args, **kwargs ): + DirectObject.__init__( self ) + + # Default camera to base camera if None is specified + self.camera = kwargs.pop( 'camera', base.camera ) + + # Default root node to render if None is specified + self.rootNp = kwargs.pop( 'rootNp', render ) + + # Default root 2d node to render2d if None is specified + self.root2d = kwargs.pop( 'root2d', render2d ) + + # Default root aspect 2d node to aspect2d if None is specified + self.rootA2d = kwargs.pop( 'rootA2d', aspect2d ) + + # Default root pixel 2d node to pixel2d if None is specified + self.rootP2d = kwargs.pop( 'rootP2d', pixel2d ) + + # Default win to base.win if None specified. + self.win = kwargs.pop( 'win', base.win ) + + # Default mouse watcher node to base.win if None specified. + self.mouseWatcherNode = kwargs.pop( 'mouseWatcherNode', + base.mouseWatcherNode ) + + \ No newline at end of file diff --git a/src/p3d/pandaBehaviour.py b/src/p3d/pandaBehaviour.py new file mode 100644 index 0000000..e881f2d --- /dev/null +++ b/src/p3d/pandaBehaviour.py @@ -0,0 +1,29 @@ +from singleTask import SingleTask +from pandaManager import PandaManager as pMgr + + +class PandaBehaviour( SingleTask ): + + def __init__( self, *args, **kwargs ): + SingleTask.__init__( self, *args, **kwargs ) + + self.accept( pMgr.PANDA_BEHAVIOUR_INIT, self.Init ) + self.accept( pMgr.PANDA_BEHAVIOUR_START, self.Start ) + self.accept( pMgr.PANDA_BEHAVIOUR_STOP, self.Stop ) + self.accept( pMgr.PANDA_BEHAVIOUR_DEL, self.Del ) + + def __del__( self ): + print ' PandaBehaviour: ', self.name, ' DELETED' + + def OnInit( self ): + pass + + def Init( self ): + self.OnInit() + + def OnDel( self ): + pass + + def Del( self ): + self.OnDel() + \ No newline at end of file diff --git a/src/p3d/pandaManager.py b/src/p3d/pandaManager.py new file mode 100644 index 0000000..9009b0b --- /dev/null +++ b/src/p3d/pandaManager.py @@ -0,0 +1,53 @@ +import os +import weakref + +import utils + + +class PandaManager( object ):#utils.Singleton ): + + PANDA_BEHAVIOUR_INIT = 'PandaBehaviourInit' + PANDA_BEHAVIOUR_START = 'PandaBehaviourStart' + PANDA_BEHAVIOUR_STOP = 'PandaBehaviourStop' + PANDA_BEHAVIOUR_DEL = 'PandaBehaviourDel' + + def __init__( self ): + #utils.Singleton.__init__( self ) + + if not hasattr( self, 'initialised' ): + self.pObjs = {} + + # Set initialise flag + self.initialised = True + + def Init( self ): + messenger.send( self.PANDA_BEHAVIOUR_INIT ) + + def Start( self ): + messenger.send( self.PANDA_BEHAVIOUR_START ) + + def Stop( self ): + messenger.send( self.PANDA_BEHAVIOUR_STOP ) + + def Del( self ): + messenger.send( self.PANDA_BEHAVIOUR_DEL ) + + def RegisterScript( self, filePath, pObj ): + """ + Register the script and the instance. Make sure to register the .py + file, not a .pyo or .pyc file. + """ + filePath = os.path.splitext( filePath )[0] + '.py' + self.pObjs.setdefault( filePath, weakref.WeakSet( [] ) ) + self.pObjs[filePath].add( pObj ) + + def ReloadScripts( self, scriptPaths ): + """ + Reload the scripts at the indicated file paths. This means reloading + the code and also recreating any objects that were attached to node + paths in the scene. + """ + scriptPaths = set( scriptPaths ) & set( self.pObjs.keys() ) + for scriptPath in scriptPaths: + for pObj in self.pObjs[scriptPath]: + pObj.ReloadScript( scriptPath ) \ No newline at end of file diff --git a/src/p3d/pandaObject.py b/src/p3d/pandaObject.py new file mode 100644 index 0000000..3d0e4d0 --- /dev/null +++ b/src/p3d/pandaObject.py @@ -0,0 +1,206 @@ +import os +import imp +import sys +import inspect + +from direct.actor.Actor import Actor +from direct.showbase.DirectObject import DirectObject + +import p3d + + +TAG_PANDA_OBJECT = 'PandaObject' + + +class PandaObject( object ): + + """ + Basic building block class, designed to be attached to a node path in the + scene graph. Doing this creates a circular reference, so care must be + taken to clear the python tag on this class' node path before trying to + remove it. + """ + + def __init__( self, np ): + + # Store the node path with a reference to this class attached to it + self.np = np + self.np.setPythonTag( TAG_PANDA_OBJECT, self ) + + self.actor = False + self.instances = {} + + def __del__( self ): + print TAG_PANDA_OBJECT, ' : ', self.np.getName(), ' DELETED' + + @staticmethod + def Get( np ): + + # Return the panda object for the supplied node path + return np.getPythonTag( TAG_PANDA_OBJECT ) + + @staticmethod + def Break( np ): + + # Detach each script from the object + pObj = PandaObject.Get( np ) + if pObj is not None and hasattr( pObj, 'instances' ): + for clsName in pObj.instances.keys(): + pObj.DetachScript( clsName ) + + # Clear the panda object tag to allow for proper garbage collection + np.clearPythonTag( TAG_PANDA_OBJECT ) + + @classmethod + def Duplicate( cls, np ): + + # Get the panda object for the input node path + pObj = np.getPythonTag( TAG_PANDA_OBJECT ) + if pObj is None: + return None + + # Duplicate the panda object then iterate over the attached instances + # and recreate them on the duplicate + dupePObj = cls( np ) + for instance in pObj.instances.values(): + instance = dupePObj.AttachScript( inspect.getfile( instance.__class__ ) ) + clsName = instance.__class__.__name__ + + # Copy property values over + for propName in pObj.GetInstanceProperties( instance ): + value = getattr( instance, propName ) + setattr( dupePObj.instances[clsName], propName, value ) + + return dupePObj + + def AttachScript( self, filePath ): + + # Make sure we're dealing with forward slashes + filePath = filePath.replace( '\\', '/' ) + + # Get the name of the module + filePath = os.path.normpath( filePath ) + head, tail = os.path.split( filePath ) + name = os.path.splitext( tail )[0] + + # If the script path is not absolute then we'll need to search for it. + # imp.find_module won't take file paths so create a list of search + # paths by joining the tail of file path to each path in sys.path. + mod = None + dirPath = os.path.split( filePath )[0] + if not os.path.isabs( filePath ): + for sysPath in sys.path: + testPath = os.path.join( sysPath, dirPath ) + sys.path.insert( 0, testPath ) + print 'testing: ', name, ' : ', testPath + + try: + #if name in sys.modules: + # del name + mod = __import__( name ) + reload( mod ) # Might need to refresh... + except: + pass + finally: + sys.path.pop( 0 ) + + print 'MOD: ', mod + if mod is not None: + break + else: + sys.path.insert( 0, dirPath ) + mod = __import__( name ) + reload( mod ) # Might need to refresh... + sys.path.pop( 0 ) + + if mod is None: + raise ImportError, ( 'No module named ' + name ) + return None + + # Get the class matching the name of the file, attach it to + # the object + clsName = name[0].upper() + name[1:] + cls = getattr( mod, clsName ) + + # Save the instance by the class name + instance = cls( clsName ) + self.instances[clsName] = instance + + # Set some basic attributes + setattr( cls, 'np', self.np ) + + # Register the script path with the panda manager. Use inspect to make + # sure we get the full script path instead of a relative path. + #scriptPath = inspect.getfile( instance.__class__ ) + #p3d.PandaManager().RegisterScript( scriptPath, self ) + + return instance + + def DetachScript( self, clsName ): + + # Remove an instance from the instance dictionary by its class name. + # Make sure to call ignoreAll() on all instances attached to this + # object which inherit from DirectObject or else they won't be + # deleted properly. + if clsName in self.instances: + instance = self.instances[clsName] + if isinstance( instance, DirectObject ): + instance.ignoreAll() + del self.instances[clsName] + + def ReloadScript( self, scriptPath ): + + # Get the class name + head, tail = os.path.split( scriptPath ) + name = os.path.splitext( tail )[0] + clsName = name[0].upper() + name[1:] + + # Get the old instance + oldInst = self.instances[clsName] + + # Remove the old object and recreate it + self.DetachScript( clsName ) + self.AttachScript( scriptPath ) + + # Go through the old instance properties and set them on the + # new instance + newInst = self.instances[clsName] + for pName, pType in self.GetInstanceProperties( oldInst ).items(): + pValue = getattr( oldInst, pName ) + setattr( newInst, pName, pValue ) + + def GetInstanceProperties( self, instance ): + + # Return a dictionary with keys being the name of the property, and + # values the actual property + props = {} + + for propName, prop in vars( instance.__class__ ).items(): + if type( prop ) == type: + props[propName] = prop + + return props + + def CreateActor( self ): + + # Turn the node path into an actor + actor = Actor( self.np ) + actor.reparentTo( self.np.getParent() ) + actor.setTransform( self.np.getTransform() ) + actor.setPythonTag( TAG_PANDA_OBJECT, self ) + + # Fix scripts to point to the new actor node path + for script in self.instances.values(): + script.np = actor + + # Clear and detach old node path + self.np.clearPythonTag( TAG_PANDA_OBJECT ) + self.np.detachNode() + + # Get the file path to the model + self.actorFilePath = str( self.np.node().getFullpath() ) + + self.np = actor + self.actor = True + + return self.np \ No newline at end of file diff --git a/src/p3d/singleTask.py b/src/p3d/singleTask.py new file mode 100644 index 0000000..81b4801 --- /dev/null +++ b/src/p3d/singleTask.py @@ -0,0 +1,67 @@ +from object import Object +from direct.task import Task + + +class SingleTask( Object ): + + def __init__( self, name, *args, **kwargs ): + Object.__init__( self, *args, **kwargs ) + + self.name = name + self._task = None + + def OnUpdate( self, task ): + """Override this function with code to be executed each frame.""" + pass + + def Update( self, task ): + """ + Run OnUpdate method - return task.cont if there was no return value. + """ + result = self.OnUpdate( task ) + if result is None: + return task.cont + + return result + + def OnStart( self ): + """ + Override this function with code to be executed when the object is + started. + """ + pass + + def Start( self, sort=None, priority=None, delayTime=0 ): + """Start the object's task if it hasn't been already.""" + # Run OnStart method + self.OnStart() + + if self._task not in taskMgr.getAllTasks(): + if not delayTime: + self._task = taskMgr.add( self.Update, '%sUpdate' % self.name, sort=sort, priority=priority ) + else: + self._task = taskMgr.doMethodLater( delayTime, self.Update, '%sUpdate' % self.name, sort=sort, priority=priority ) + + def OnStop( self ): + """ + Override this function with code to be executed when the object is + stopped. + """ + pass + + def Stop( self ): + """Remove the object's task from the task manager.""" + # Run OnStop method + self.OnStop() + + if self._task in taskMgr.getAllTasks(): + taskMgr.remove( self._task ) + self._task = None + + def IsRunning( self ): + """ + Return True if the object's task can be found in the task manager, + False otherwise. + """ + return self._task in taskMgr.getAllTasks() + \ No newline at end of file diff --git a/src/p3d/wxPanda.py b/src/p3d/wxPanda.py new file mode 100644 index 0000000..d41fe2d --- /dev/null +++ b/src/p3d/wxPanda.py @@ -0,0 +1,133 @@ +import sys + +import wx +from direct.showbase.DirectObject import DirectObject +from pandac.PandaModules import WindowProperties + + +keyCodes = { + wx.WXK_SPACE:'space', + wx.WXK_DELETE:'del', + wx.WXK_ESCAPE:'escape', + wx.WXK_BACK:'backspace', + wx.WXK_CONTROL:'control', + wx.WXK_ALT:'alt', + wx.WXK_UP:'arrow_up', + wx.WXK_DOWN:'arrow_down', + wx.WXK_LEFT:'arrow_left', + wx.WXK_RIGHT:'arrow_right' +} + + +def OnKey( evt, action='' ): + """ + Bind this method to a wx.EVT_KEY_XXX event coming from a wx panel or other + widget, and it will stop wx 'eating' the event and passing it to Panda's + base class instead. + """ + keyCode = evt.GetKeyCode() + if keyCode in keyCodes: + messenger.send( keyCodes[keyCode] + action ) + elif keyCode in range( 256 ): + + # Test for any other modifiers. Add these in the order shift, control, + # alt + mod = '' + if evt.ShiftDown(): + mod += 'shift-' + if evt.ControlDown(): + mod += 'control-' + if evt.AltDown(): + mod += 'alt-' + char = chr( keyCode ).lower() + messenger.send( mod + char + action ) + +def OnKeyUp( evt ): + OnKey( evt, '-up' ) + + +def OnKeyDown( evt ): + OnKey( evt ) + + +def OnLeftUp( evt ): + messenger.send( 'mouse1-up' ) + + +class Viewport( wx.Panel ): + + def __init__( self, *args, **kwargs ): + """ + Initialise the wx panel. You must complete the other part of the + init process by calling Initialize() once the wx-window has been + built. + """ + wx.Panel.__init__( self, *args, **kwargs ) + + self._win = None + + def GetWindow( self ): + return self._win + + def SetWindow( self, win ): + self._win = win + + def Initialize( self, useMainWin=True ): + """ + The panda3d window must be put into the wx-window after it has been + shown, or it will not size correctly. + """ + assert self.GetHandle() != 0 + wp = WindowProperties() + wp.setOrigin( 0, 0 ) + wp.setSize( self.ClientSize.GetWidth(), self.ClientSize.GetHeight() ) + wp.setParentWindow( self.GetHandle() ) + if self._win is None: + if useMainWin: + base.openDefaultWindow( props=wp, gsg=None ) + self._win = base.win + else: + self._win = base.openWindow( props=wp, makeCamera=0 ) + self.Bind( wx.EVT_SIZE, self.OnResize ) + + def OnResize( self, event ): + """When the wx-panel is resized, fit the panda3d window into it.""" + frame_size = event.GetSize() + wp = WindowProperties() + wp.setOrigin( 0, 0 ) + wp.setSize( frame_size.GetWidth(), frame_size.GetHeight() ) + self._win.requestProperties( wp ) + + +class App( wx.App, DirectObject ): + + """ + Don't forget to bind your frame's wx.EVT_CLOSE event to the app's + self.Quit method, or the application will not close properly. + """ + + def ReplaceEventLoop( self ): + self.evtLoop = wx.EventLoop() + self.oldLoop = wx.EventLoop.GetActive() + wx.EventLoop.SetActive( self.evtLoop ) + taskMgr.add( self.WxStep, 'evtLoopTask' ) + self.WxStep() + + def OnDestroy( self, event=None ): + self.WxStep() + wx.EventLoop.SetActive( self.oldLoop ) + + def Quit( self, event=None ): + self.OnDestroy( event ) + try: + base + except NameError: + sys.exit() + base.userExit() + + def WxStep( self, task=None ): + while self.evtLoop.Pending(): + self.evtLoop.Dispatch() + self.ProcessIdle() + if task != None: + return task.cont \ No newline at end of file diff --git a/src/pandaEditor/__init__.py b/src/pandaEditor/__init__.py new file mode 100644 index 0000000..100a8ff --- /dev/null +++ b/src/pandaEditor/__init__.py @@ -0,0 +1,23 @@ +import os +import sys + + +# Make sure game and editor can be found on sys.path. +pandaEditorPath = os.path.abspath( 'pandaEditor' ) +if pandaEditorPath not in sys.path: + sys.path.append( pandaEditorPath ) + + +from scene import Scene +from showBase import ShowBase +from selection import Selection +from project import Project + +import ui +import game +import editor +import gizmos +import actions + + +from app import App \ No newline at end of file diff --git a/src/pandaEditor/actions/__init__.py b/src/pandaEditor/actions/__init__.py new file mode 100644 index 0000000..53c1390 --- /dev/null +++ b/src/pandaEditor/actions/__init__.py @@ -0,0 +1,11 @@ +from base import Base +from manager import Manager + +from add import Add +from remove import Remove +from select import Select +from deselect import Deselect +from composite import Composite +from setAttribute import SetAttribute +from transform import Transform +from parent import Parent \ No newline at end of file diff --git a/src/pandaEditor/actions/add.py b/src/pandaEditor/actions/add.py new file mode 100644 index 0000000..265c09e --- /dev/null +++ b/src/pandaEditor/actions/add.py @@ -0,0 +1,17 @@ +import p3d +from base import Base + + +class Add( Base ): + + def __init__( self, app, nps ): + self.app = app + self.nps = nps + + def Undo( self ): + self.app.scene.DeleteNodePaths( self.nps ) + self.app.doc.OnModified() + + def Redo( self ): + self.app.scene.AddNodePaths( self.nps ) + self.app.doc.OnModified() \ No newline at end of file diff --git a/src/pandaEditor/actions/base.py b/src/pandaEditor/actions/base.py new file mode 100644 index 0000000..9211b34 --- /dev/null +++ b/src/pandaEditor/actions/base.py @@ -0,0 +1,17 @@ +class Base( object ): + + def __init__( self, modify=False, modifySelection=False ): + self.modify = modify + self.modifySelection = modifySelection + + def __call__( self ): + self.Redo() + + def Undo( self ): + pass + + def Redo( self ): + pass + + def Destroy( self ): + pass \ No newline at end of file diff --git a/src/pandaEditor/actions/composite.py b/src/pandaEditor/actions/composite.py new file mode 100644 index 0000000..1985de3 --- /dev/null +++ b/src/pandaEditor/actions/composite.py @@ -0,0 +1,19 @@ +from base import Base + + +class Composite( Base ): + + def __init__( self, actions ): + self.actions = actions + + def Undo( self ): + for actn in reversed( self.actions ): + actn.Undo() + + def Redo( self ): + for actn in self.actions: + actn.Redo() + + def Destroy( self ): + for actn in self.actions: + actn.Destroy() \ No newline at end of file diff --git a/src/pandaEditor/actions/deselect.py b/src/pandaEditor/actions/deselect.py new file mode 100644 index 0000000..be7c1f4 --- /dev/null +++ b/src/pandaEditor/actions/deselect.py @@ -0,0 +1,19 @@ +from base import Base + + +class Deselect( Base ): + + def __init__( self, app, nps ): + self.app = app + + # Adjust node path list to represent only those which are selected + # at the time this class is instanced + self.nps = list( set( nps ) & set( self.app.selection.nps ) ) + + def Undo( self ): + self.app.selection.Add( self.nps ) + self.app.doc.OnSelectionChanged() + + def Redo( self ): + self.app.selection.Remove( self.nps ) + self.app.doc.OnSelectionChanged() \ No newline at end of file diff --git a/src/pandaEditor/actions/manager.py b/src/pandaEditor/actions/manager.py new file mode 100644 index 0000000..9e5b7d1 --- /dev/null +++ b/src/pandaEditor/actions/manager.py @@ -0,0 +1,40 @@ +class Manager: + def __init__( self ): + self.undoList = [] + self.redoList = [] + + def Reset( self ): + while self.undoList: + actn = self.undoList.pop() + actn.Destroy() + + while self.redoList: + actn = self.redoList.pop() + actn.Destroy() + + def Push( self, actn ): + #print 'PUSH: ', actn + self.undoList.append( actn ) + #if self.redoList: + # self.redoList.pop() + while self.redoList: + actn = self.redoList.pop() + actn.Destroy() + + def Undo( self ): + if len( self.undoList ) < 1: + print 'No more undo' + else: + actn = self.undoList.pop() + self.redoList.append( actn ) + actn.Undo() + #print 'UNDO: ', actn + + def Redo( self ): + if len( self.redoList ) < 1: + print 'No more redo' + else: + actn = self.redoList.pop() + self.undoList.append( actn ) + actn.Redo() + #print 'REDO: ', actn \ No newline at end of file diff --git a/src/pandaEditor/actions/parent.py b/src/pandaEditor/actions/parent.py new file mode 100644 index 0000000..328518d --- /dev/null +++ b/src/pandaEditor/actions/parent.py @@ -0,0 +1,21 @@ +from base import Base + + +class Parent( Base ): + + def __init__( self, app, nps, parent ): + self.app = app + self.nps = nps + self.parent = parent + + self.oldParents = [np.getParent() for np in self.nps] + + def Undo( self ): + for i in range( len( self.nps ) ): + self.nps[i].wrtReparentTo( self.oldParents[i] ) + self.app.doc.OnModified() + + def Redo( self ): + for np in self.nps: + np.wrtReparentTo( self.parent ) + self.app.doc.OnModified() \ No newline at end of file diff --git a/src/pandaEditor/actions/remove.py b/src/pandaEditor/actions/remove.py new file mode 100644 index 0000000..0d732a8 --- /dev/null +++ b/src/pandaEditor/actions/remove.py @@ -0,0 +1,17 @@ +import p3d +from base import Base + + +class Remove( Base ): + + def __init__( self, app, nps ): + self.app = app + self.nps = nps + + def Undo( self ): + self.app.scene.AddNodePaths( self.nps ) + self.app.doc.OnModified() + + def Redo( self ): + self.app.scene.DeleteNodePaths( self.nps ) + self.app.doc.OnModified() \ No newline at end of file diff --git a/src/pandaEditor/actions/select.py b/src/pandaEditor/actions/select.py new file mode 100644 index 0000000..4b70277 --- /dev/null +++ b/src/pandaEditor/actions/select.py @@ -0,0 +1,16 @@ +from base import Base + + +class Select( Base ): + + def __init__( self, app, nps ): + self.app = app + self.nps = nps + + def Undo( self ): + self.app.selection.Remove( self.nps ) + self.app.doc.OnSelectionChanged() + + def Redo( self ): + self.app.selection.Add( self.nps ) + self.app.doc.OnSelectionChanged() \ No newline at end of file diff --git a/src/pandaEditor/actions/setAttribute.py b/src/pandaEditor/actions/setAttribute.py new file mode 100644 index 0000000..bb6d349 --- /dev/null +++ b/src/pandaEditor/actions/setAttribute.py @@ -0,0 +1,30 @@ +from base import Base + + +class SetAttribute( Base ): + + def __init__( self, app, nps, attr, val ): + self.app = app + self.nps = nps + self.attr = attr + self.val = val + + # Save old values + self.oldVals = [] + for np in self.nps: + self.oldVals.append( attr.Get( np ) ) + + def Undo( self ): + """Undo the action.""" + for i in range( len( self.nps ) ): + self.attr.Set( self.nps[i], self.oldVals[i] ) + + self.app.doc.OnModified() + #self.app.doc.OnSelectionChanged() + + def Redo( self ): + """Redo the action.""" + for np in self.nps: + self.attr.Set( np, self.val ) + self.app.doc.OnModified() + #self.app.doc.OnSelectionChanged() \ No newline at end of file diff --git a/src/pandaEditor/actions/transform.py b/src/pandaEditor/actions/transform.py new file mode 100644 index 0000000..5123ce8 --- /dev/null +++ b/src/pandaEditor/actions/transform.py @@ -0,0 +1,22 @@ +from base import Base + + +class Transform( Base ): + + def __init__( self, app, nps, xforms, oldXforms ): + self.app = app + self.nps = nps + self.xforms = xforms + self.oldXforms = oldXforms + + def Undo( self ): + for i in range( len( self.nps ) ): + self.nps[i].setTransform( self.oldXforms[i] ) + self.app.doc.OnModified() + self.app.doc.OnSelectionChanged() + + def Redo( self ): + for i in range( len( self.nps ) ): + self.nps[i].setTransform( self.xforms[i] ) + self.app.doc.OnModified() + self.app.doc.OnSelectionChanged() \ No newline at end of file diff --git a/src/pandaEditor/app.py b/src/pandaEditor/app.py new file mode 100644 index 0000000..40af16c --- /dev/null +++ b/src/pandaEditor/app.py @@ -0,0 +1,279 @@ +import os +import traceback + +from direct.directtools.DirectGrid import DirectGrid +from wx.lib.pubsub import Publisher as pub +import pandac.PandaModules as pm +import panda3d.core as pc + +import p3d +import wxExtra +import ui +import editor +import gizmos +import actions +import commands as cmds +from scene import Scene +from showBase import ShowBase +from selection import Selection +from project import Project + + +class App( p3d.wx.App ): + + """Base editor class.""" + + def OnInit( self ): + self.gizmo = False + self._fooTask = None + + # Bind publisher events + pub.subscribe( self.OnUpdate, 'Update' ) + pub.subscribe( self.OnUpdateSelection, 'UpdateSelection' ) + + # Build main frame, start Panda and replace the wx event loop with + # Panda's. + self.frame = ui.MainFrame( None, size=(800, 600) ) + ShowBase( self.frame.pnlGameView, self.frame.pnlEdView ) + self.ReplaceEventLoop() + + # Create project manager + self.project = Project( self ) + self.frame.SetProjectPath( self.frame.cfg.Read( 'projDirPath' ) ) + + # Create grid + self.grid = DirectGrid( + parent=base.edRender, + planeColor=(0.5, 0.5, 0.5, 0.5) + ) + + # Create frame rate meter + self.frameRate = p3d.FrameRate() + + # Create shading mode keys + dsp = p3d.DisplayShading() + dsp.accept( '4', dsp.Wireframe ) + dsp.accept( '5', dsp.Shade ) + dsp.accept( '6', dsp.Texture ) + + # Set up gizmos + self.SetupGizmoManager() + + # Bind mouse events + self.accept( 'mouse1', self.OnMouse1Down ) + self.accept( 'shift-mouse1', self.OnMouse1Down, [True] ) + self.accept( 'mouse2', self.OnMouse2Down ) + self.accept( 'mouse1-up', self.OnMouse1Up ) + self.accept( 'mouse2-up', self.OnMouse2Up ) + + # Create selection manager + self.selection = Selection( + camera=base.edCamera, + root2d=base.edRender2d, + win=base.edWin, + mouseWatcherNode=base.edMouseWatcherNode + ) + + # Create actions manager which will control the undo queue. + self.actnMgr = actions.Manager() + + # Bind events + self.accept( 'z', self.actnMgr.Undo ) + self.accept( 'shift-z', self.actnMgr.Redo ) + self.accept( 'f', self.FrameSelection ) + self.accept( 'del', lambda fn: cmds.Remove( fn() ), [self.selection.Get] ) + self.accept( 'backspace', lambda fn: cmds.Remove( fn() ), [self.selection.Get] ) + self.accept( 'control-d', lambda fn: cmds.Duplicate( fn() ), [self.selection.Get] ) + self.accept( 'arrow_up', lambda fn: cmds.Select( fn() ), [self.selection.SelectParent] ) + self.accept( 'arrow_down', lambda fn: cmds.Select( fn() ), [self.selection.SelectChild] ) + self.accept( 'arrow_left', lambda fn: cmds.Select( fn() ), [self.selection.SelectPrev] ) + self.accept( 'arrow_right', lambda fn: cmds.Select( fn() ), [self.selection.SelectNext] ) + self.accept( 'projectFilesModified', self.OnProjectFilesModified ) + + # DEBUG + self.fileTypes = { + '.egg':self.AddModel, + '.bam':self.AddModel, + '.pz':self.AddModel, + '.sha':self.AddShader + } + + # Create a "game" + self.game = editor.Base() + self.game.OnInit() + + # Start with a new scene + self.CreateScene() + self.doc.OnRefresh() + + self.frame.Show( True ) + + return True + + def SetupGizmoManager( self ): + """Create gizmo manager.""" + gizmoMgrRootNp = base.edRender.attachNewNode( 'gizmoManager' ) + kwargs = { + 'camera':base.edCamera, + 'rootNp':gizmoMgrRootNp, + 'win':base.edWin, + 'mouseWatcherNode':base.edMouseWatcherNode + } + self.gizmoMgr = gizmos.Manager( **kwargs ) + self.gizmoMgr.AddGizmo( gizmos.Translation( 'pos', **kwargs ) ) + self.gizmoMgr.AddGizmo( gizmos.Rotation( 'rot', **kwargs ) ) + self.gizmoMgr.AddGizmo( gizmos.Scale( 'scl', **kwargs ) ) + + # Bind gizmo manager events + self.accept( 'q', self.gizmoMgr.SetActiveGizmo, [None] ) + self.accept( 'w', self.gizmoMgr.SetActiveGizmo, ['pos'] ) + self.accept( 'e', self.gizmoMgr.SetActiveGizmo, ['rot'] ) + self.accept( 'r', self.gizmoMgr.SetActiveGizmo, ['scl'] ) + self.accept( 'space', self.gizmoMgr.ToggleLocal ) + self.accept( '+', self.gizmoMgr.SetSize, [2] ) + self.accept( '-', self.gizmoMgr.SetSize, [0.5] ) + + def OnMouse1Down( self, shift=False ): + """ + Handle mouse button 1 down event. Start the drag select operation if + a gizmo is not being used and the alt key is not down, otherwise start + the transform operation. + """ + if ( not self.gizmoMgr.IsDragging() and + p3d.MOUSE_ALT not in base.edCamera.mouse.modifiers ): + self.selection.StartDragSelect( shift ) + elif self.gizmoMgr.IsDragging(): + self.StartTransform() + + def OnMouse2Down( self ): + """ + Handle mouse button 2 down event. Start the transform operation if a + gizmo is being used. + """ + if self.gizmoMgr.IsDragging(): + self.StartTransform() + + def OnMouse1Up( self ): + """ + Handle mouse button 1 up event. Stop the drag select operation if the + marquee is running, otherwise stop the transform operation if a gizmo + is being used. + """ + if self.selection.marquee.IsRunning(): + + # Don't perform selection if there are no nodes and the selection + # is currently empty. + selNodes = self.selection.StopDragSelect() + if self.selection.nps or selNodes: + cmds.Select( selNodes ) + elif self.gizmoMgr.IsDragging() or self.gizmo: + self.StopTransform() + + def OnMouse2Up( self ): + """ + Handle mouse button 2 up event. Stop the transform operation if a + gizmo is being used. + """ + if self.gizmoMgr.IsDragging() or self.gizmo: + self.StopTransform() + + def StartTransform( self ): + """ + Start the transfrom operation by adding a task to constantly send a + selection modified message while transfoming. + """ + self._fooTask = taskMgr.add( self.doc.OnSelectionModified, 'SelectionModified' ) + self.gizmo = True + + def StopTransform( self ): + """ + Stop the transfrom operation by removing the selection modified + message task. Also create a transform action and push it onto the undo + queue. + """ + actGizmo = self.gizmoMgr.GetActiveGizmo() + nps = actGizmo.attachedNps + xforms = [np.getTransform() for np in nps] + actn = actions.Transform( self, nps, xforms, actGizmo.initNpXforms ) + self.actnMgr.Push( actn ) + + # Remove the transform task + if self._fooTask in taskMgr.getAllTasks(): + taskMgr.remove( self._fooTask ) + self._fooTask = None + + self.gizmo = False + self.doc.OnModified() + + def FrameSelection( self ): + """ + Call frame selection on the camera if there are some node paths in the + selection. + """ + if self.selection.nps: + base.edCamera.Frame( self.selection.nps ) + + def OnUpdate( self, msg ): + self.selection.Update() + + def OnUpdateSelection( self, msg ): + """ + Subscribed to the update selection message. Make sure that the + selected nodes are attached to the managed gizmos, then refresh the + active one. + """ + self.gizmoMgr.AttachNodePaths( msg.data ) + self.gizmoMgr.RefreshActiveGizmo() + + def CreateScene( self, filePath=None, newDoc=True ): + """ + Create an empty scene and set its root node to the picker's root node. + """ + # Reset undo queue if creating a new document + if newDoc: + self.actnMgr.Reset() + + # Close the current scene if there is one + if hasattr( self, 'scene' ): + self.game.pluginMgr.OnSceneClose() + self.scene.Close() + + # Create a new scene + self.scene = Scene( self, filePath=filePath, camera=base.edCamera ) + self.scene.rootNp.reparentTo( base.edRender ) + + # Set the selection and picker root node to the scene's root node + self.selection.rootNp = self.scene.rootNp + self.selection.picker.rootNp = self.scene.rootNp + self.selection.Clear() + + # Create the document wrapper if creating a new document + if newDoc: + self.doc = ui.Document( self.scene ) + self.doc.OnSelectionChanged() + + def OnDragDrop( self, filePath ): + + # Get the object under the mouse, if any + np = self.selection.GetNodePathUnderMouse() + self.AddFile( filePath, np ) + + def AddFile( self, filePath, np=None ): + ext = os.path.splitext( filePath )[1] + if ext in self.fileTypes: + fn = self.fileTypes[ext] + fn( filePath, np ) + + def AddModel( self, filePath, np=None ): + np = base.game.nodeMgr.Create( 'ModelRoot', filePath ) + cmds.Add( [np] ) + + def AddShader( self, filePath, np=None ): + pandaPath = pm.Filename.fromOsSpecific( filePath ) + shdr = pc.Shader.load( pandaPath ) + + # BROKEN + self.SetAttribute( np.setShader, shdr, np.getShader(), np.clearShader ) + + def OnProjectFilesModified( self, filePaths ): + self.game.pluginMgr.OnProjectFilesModified( filePaths ) \ No newline at end of file diff --git a/src/pandaEditor/commands.py b/src/pandaEditor/commands.py new file mode 100644 index 0000000..7fc1f1b --- /dev/null +++ b/src/pandaEditor/commands.py @@ -0,0 +1,82 @@ +import wx + +import actions + + +def Add( nps ): + """ + Create the add composite action, execute it and push it onto the + undo queue. + """ + actns = [ + actions.Deselect( wx.GetApp(), wx.GetApp().selection.nps ), + actions.Add( wx.GetApp(), nps ), + actions.Select( wx.GetApp(), nps ) + ] + comp = actions.Composite( actns ) + wx.GetApp().actnMgr.Push( comp ) + comp() + + +def Duplicate( nps ): + """ + Duplicate the indicated node paths once they've been deselected, then + create an add action and push it onto the undo queue. + """ + # Record the current selection then clear it + selNps = wx.GetApp().selection.nps + wx.GetApp().selection.Clear() + + # Duplicate the indicated node paths + dupeNps = wx.GetApp().scene.DuplicateNodePaths( selNps ) + + # Reset the selection and run add + wx.GetApp().selection.Add( selNps ) + Add( dupeNps ) + + +def Remove( nps ): + """ + Create the remove composite action, execute it and push it onto the + undo queue. + """ + actns = [ + actions.Deselect( wx.GetApp(), nps ), + actions.Remove( wx.GetApp(), nps ) + ] + comp = actions.Composite( actns ) + wx.GetApp().actnMgr.Push( comp ) + comp() + + +def Select( nps ): + """ + Create the select composite action, execute it and push it onto the + undo queue. + """ + actns = [ + actions.Deselect( wx.GetApp(), wx.GetApp().selection.nps ), + actions.Select( wx.GetApp(), nps ) + ] + comp = actions.Composite( actns ) + wx.GetApp().actnMgr.Push( comp ) + comp() + + +def SetAttribute( nps, attr, val ): + """ + Create the set attribute action, execute it and push it onto the undo + queue. + """ + actn = actions.SetAttribute( wx.GetApp(), nps, attr, val ) + wx.GetApp().actnMgr.Push( actn ) + actn() + + +def Parent( nps, parent ): + """ + Create the parent action, execute it and push it onto the undo queue. + """ + actn = actions.Parent( wx.GetApp(), nps, parent ) + wx.GetApp().actnMgr.Push( actn ) + actn() \ No newline at end of file diff --git a/src/pandaEditor/editor/__init__.py b/src/pandaEditor/editor/__init__.py new file mode 100644 index 0000000..d892ada --- /dev/null +++ b/src/pandaEditor/editor/__init__.py @@ -0,0 +1,7 @@ +import sys +import game +sys.modules['oldGame'] = sys.modules.pop( 'game' ) + +import nodes +import plugins +from base import Base diff --git a/src/pandaEditor/editor/base.py b/src/pandaEditor/editor/base.py new file mode 100644 index 0000000..2dcb9e1 --- /dev/null +++ b/src/pandaEditor/editor/base.py @@ -0,0 +1,13 @@ +import game +import plugins +from sceneParser import SceneParser + + +class Base( game.Base ): + + def __init__( self, *args, **kwargs ): + game.Base.__init__( self, *args, **kwargs ) + + # Use editor versions for some systems. + self.pluginMgr = plugins.Manager( self ) + self.scnParser = SceneParser() \ No newline at end of file diff --git a/src/pandaEditor/editor/nodes/__init__.py b/src/pandaEditor/editor/nodes/__init__.py new file mode 100644 index 0000000..9a8a17f --- /dev/null +++ b/src/pandaEditor/editor/nodes/__init__.py @@ -0,0 +1,7 @@ +import sys + +from constants import * +import nodePath +sys.modules['game.nodes.nodePath'] = nodePath +import lensNode +sys.modules['game.nodes.lensNode'] = lensNode \ No newline at end of file diff --git a/src/pandaEditor/editor/nodes/constants.py b/src/pandaEditor/editor/nodes/constants.py new file mode 100644 index 0000000..3b78683 --- /dev/null +++ b/src/pandaEditor/editor/nodes/constants.py @@ -0,0 +1,2 @@ +TAG_IGNORE = 'P3D_IgnoreNode' +TAG_PICKABLE = 'P3D_PickableNode' \ No newline at end of file diff --git a/src/pandaEditor/editor/nodes/lensNode.py b/src/pandaEditor/editor/nodes/lensNode.py new file mode 100644 index 0000000..7e236d2 --- /dev/null +++ b/src/pandaEditor/editor/nodes/lensNode.py @@ -0,0 +1,16 @@ +import pandac.PandaModules as pm + +from constants import * +from game.nodes.lensNode import LensNode as GameLensNode + + +class LensNode( GameLensNode ): + + def OnSelect( self, np ): + children = set( np.getChildren() ) + np.node().showFrustum() + frustum = list( set( np.getChildren() ) - children )[0] + frustum.setPythonTag( TAG_IGNORE, True ) + + def OnDeselect( self, np ): + np.node().hideFrustum() \ No newline at end of file diff --git a/src/pandaEditor/editor/nodes/nodePath.py b/src/pandaEditor/editor/nodes/nodePath.py new file mode 100644 index 0000000..40fb5f4 --- /dev/null +++ b/src/pandaEditor/editor/nodes/nodePath.py @@ -0,0 +1,34 @@ +from constants import * +from game.nodes.nodePath import NodePath as GameNodePath + + +class NodePath( GameNodePath ): + + geo = None + pickable = True + + @classmethod + def SetPickable( cls, value=True ): + cls.pickable = value + + @classmethod + def SetEditorGeometry( cls, geo ): + geo.setPythonTag( TAG_IGNORE, True ) + geo.setLightOff() + geo.node().adjustDrawMask( *base.GetEditorRenderMasks() ) + cls.geo = geo + + def SetupNodePath( self, np ): + GameNodePath.SetupNodePath( self, np ) + + if self.geo is not None: + self.geo.copyTo( np ) + + if self.pickable: + np.setPythonTag( TAG_PICKABLE, self.pickable ) + + def OnSelect( self, np ): + pass + + def OnDeselect( self, np ): + pass \ No newline at end of file diff --git a/src/pandaEditor/editor/plugins/__init__.py b/src/pandaEditor/editor/plugins/__init__.py new file mode 100644 index 0000000..43b66f4 --- /dev/null +++ b/src/pandaEditor/editor/plugins/__init__.py @@ -0,0 +1,6 @@ +import sys + +import base +sys.modules['game.plugins.base'] = base + +from manager import Manager \ No newline at end of file diff --git a/src/pandaEditor/editor/plugins/base.py b/src/pandaEditor/editor/plugins/base.py new file mode 100644 index 0000000..cd495bf --- /dev/null +++ b/src/pandaEditor/editor/plugins/base.py @@ -0,0 +1,23 @@ +import wx +from oldGame.plugins.base import Base as OldBase + + +class Base( OldBase ): + + def __init__( self, *args, **kwargs ): + OldBase.__init__( self, *args, **kwargs ) + + self.app = wx.GetApp() + self.ui = self.app.frame + + def OnInit( self, *args, **kwargs ): + OldBase.OnInit( self, *args, **kwargs ) + + def OnUpdate( self, msg ): + pass + + def OnSceneClose( self ): + pass + + def OnProjectFilesModified( self, filePaths ): + pass \ No newline at end of file diff --git a/src/pandaEditor/editor/plugins/manager.py b/src/pandaEditor/editor/plugins/manager.py new file mode 100644 index 0000000..4c63129 --- /dev/null +++ b/src/pandaEditor/editor/plugins/manager.py @@ -0,0 +1,31 @@ +import traceback + +import game + + +class Manager( game.plugins.Manager ): + + def __init__( self, *args, **kwargs ): + game.plugins.Manager.__init__( self, *args, **kwargs ) + + def LoadPlugin( self, fileName ): + temp = __import__( fileName, globals(), locals(), ['editorPlugin'], -1 ) + + # Create an instance of the editor plugin to wrap the game plugin. + try: + cls = getattr( temp.editorPlugin, 'EditorPlugin' ) + return cls( self.game ) + except Exception, e: + traceback.print_exc() + + def OnSceneClose( self ): + for plugin in self.plugins: + plugin.OnSceneClose() + + def OnUpdate( self, msg ): + for plugin in self.plugins: + plugin.OnUpdate( msg ) + + def OnProjectFilesModified( self, filePaths ): + for plugin in self.plugins: + plugin.OnProjectFilesModified( filePaths ) \ No newline at end of file diff --git a/src/pandaEditor/editor/sceneParser.py b/src/pandaEditor/editor/sceneParser.py new file mode 100644 index 0000000..fddc22b --- /dev/null +++ b/src/pandaEditor/editor/sceneParser.py @@ -0,0 +1,109 @@ +import xml.etree.cElementTree as et + +import pandac.PandaModules as pm + +import p3d +import game +import utils + + +class SceneParser( game.SceneParser ): + + def __init__( self, *args, **kwargs ): + game.SceneParser.__init__( self, *args, **kwargs ) + + self.saveCastFnMap = { + bool:str, + float:str, + int:str, + str:str, + type:self.GetName, + pm.Vec2:p3d.FloatTuple2Str, + pm.LVecBase2f:p3d.FloatTuple2Str, + pm.Vec3:p3d.FloatTuple2Str, + pm.LVecBase3f:p3d.FloatTuple2Str, + pm.Vec4:p3d.FloatTuple2Str, + pm.LVecBase4f:p3d.FloatTuple2Str, + pm.Point2:p3d.FloatTuple2Str, + pm.Point3:p3d.FloatTuple2Str, + pm.Point4:p3d.FloatTuple2Str, + pm.Mat4:p3d.Mat42Str, + pm.LMatrix4f:p3d.Mat42Str + } + + def GetName( self, ttype ): + return ttype.__name__ + + def SaveData( self, wrpr, elem ): + + # Get a dictionary representing all the data for the component then + # serialise it. + dataDict = wrpr.GetData() + for key, value in dataDict.items(): + if type( value ) in self.saveCastFnMap: + castFn = self.saveCastFnMap[type( value )] + subElem = et.SubElement( elem, 'Item' ) + subElem.set( 'name', key ) + subElem.set( 'value', castFn( value ) ) + subElem.set( 'type', type( value ).__name__ ) + else: + print 'Could not save attribute: ', key, ' : of type: ', type( value ) + + # Do child components + for cWrpr in wrpr.children: + cElem = et.SubElement( elem, 'Component' ) + cElem.set( 'type', cWrpr.name ) + for key, value in cWrpr.createArgs.items(): + cElem.set( key, value ) + self.SaveData( cWrpr, cElem ) + + def SaveNode( self, np, parentElem ): + """ + TO DO: Remove dependency on 'P3D_PickableNode' tag. + """ + if np.getPythonTag( 'P3D_PickableNode' ): + + # Get the wrapper for this node type + Wrpr = base.game.nodeMgr.GetWrapper( np ) + if Wrpr is not None: + + wrpr = Wrpr( np ) + + # Create new element for this node path + elem = et.SubElement( parentElem, 'Component' ) + + # Write out node type and uuid. + nTypeStr = base.game.nodeMgr.GetTypeString( np ) + elem.set( 'type', nTypeStr ) + + # Write out create args + for key, value in wrpr.createArgs.items(): + elem.set( key, value ) + + # Recursively save the data. + self.SaveData( wrpr, elem ) + + else: + elem = parentElem + + # Recurse down the tree but ignore model root children, as serialising + # them will load them twice. + cElem = et.SubElement( elem, 'Children' ) + if type( np.node() ) != pm.ModelRoot: + for childNp in np.getChildren(): + self.SaveNode( childNp, cElem ) + + # Delete child elements if they're empty. + if not list( cElem ) and not cElem.keys(): + elem.remove( cElem ) + + def Save( self, rootNp, filePath ): + """Save the scene out to an xml file.""" + # Create root element and version info + rootElem = et.Element( 'Map' ) + self.SaveNode( rootNp, rootElem ) + + # Wrap with an element tree and write to file + tree = et.ElementTree( rootElem ) + utils.Indent( tree.getroot() ) + tree.write( filePath ) \ No newline at end of file diff --git a/src/pandaEditor/game/__init__.py b/src/pandaEditor/game/__init__.py new file mode 100644 index 0000000..ce9c0a7 --- /dev/null +++ b/src/pandaEditor/game/__init__.py @@ -0,0 +1,4 @@ +import nodes +import plugins +from base import Base +from sceneParser import SceneParser \ No newline at end of file diff --git a/src/pandaEditor/game/base.py b/src/pandaEditor/game/base.py new file mode 100644 index 0000000..f3b6458 --- /dev/null +++ b/src/pandaEditor/game/base.py @@ -0,0 +1,15 @@ +import nodes +import plugins +from sceneParser import SceneParser + + +class Base( object ): + + def __init__( self ): + base.game = self + self.nodeMgr = nodes.Manager() + self.pluginMgr = plugins.Manager( self ) + self.scnParser = SceneParser() + + def OnInit( self ): + self.pluginMgr.Load() \ No newline at end of file diff --git a/src/pandaEditor/game/nodes/__init__.py b/src/pandaEditor/game/nodes/__init__.py new file mode 100644 index 0000000..5bf38bf --- /dev/null +++ b/src/pandaEditor/game/nodes/__init__.py @@ -0,0 +1,3 @@ +from constants import * +from attributes import * +from manager import Manager \ No newline at end of file diff --git a/src/pandaEditor/game/nodes/actor.py b/src/pandaEditor/game/nodes/actor.py new file mode 100644 index 0000000..37a63d3 --- /dev/null +++ b/src/pandaEditor/game/nodes/actor.py @@ -0,0 +1,30 @@ +import pandac.PandaModules as pm + +import p3d +from constants import * +from nodePath import NodePath +from attributes import Attribute + + +class Actor( NodePath ): + + def __init__( self, *args, **kwargs ): + NodePath.__init__( self, *args, **kwargs ) + + self.attributes.append( Attribute( 'Actor' ) ) + + def Create( self, filePath ): + filePath = pm.Filename.fromOsSpecific( filePath ) + try: + np = loader.loadModel( filePath ) + except: + np = loader.loadModel( filePath + '.bam' ) + + pObj = p3d.PandaObject( np ) + pObj.CreateActor() + np.removeNode() + np = pObj.np + + np.setTag( TAG_NODE_TYPE, 'Actor' ) + + return np \ No newline at end of file diff --git a/src/pandaEditor/game/nodes/ambientLight.py b/src/pandaEditor/game/nodes/ambientLight.py new file mode 100644 index 0000000..2802b34 --- /dev/null +++ b/src/pandaEditor/game/nodes/ambientLight.py @@ -0,0 +1,10 @@ +import pandac.PandaModules as pm + +from light import Light + + +class AmbientLight( Light ): + + def __init__( self, *args, **kwargs ): + kwargs['nType'] = pm.AmbientLight + Light.__init__( self, *args, **kwargs ) \ No newline at end of file diff --git a/src/pandaEditor/game/nodes/attributes.py b/src/pandaEditor/game/nodes/attributes.py new file mode 100644 index 0000000..9464cc4 --- /dev/null +++ b/src/pandaEditor/game/nodes/attributes.py @@ -0,0 +1,64 @@ +import p3d +import pandac.PandaModules as pm + + +class Attribute( object ): + + def __init__( self, label, pType=None, GetFn=None, SetFn=None, TgtFn=None, + getArgs=[], setArgs=[], tgtArgs=[], w=True, e=True ): + self.label = label + self.type = pType + self.GetFn = GetFn + self.SetFn = SetFn + self.TgtFn = TgtFn + self.getArgs = getArgs + self.setArgs = setArgs + self.tgtArgs = tgtArgs + self.w = w + self.e = e + + self.children = [] + name = self.label.replace( ' ', '' ) + self.name = name[0].lower() + name[1:] + + def GetTarget( self, np ): + if self.TgtFn is None: + tgt = np + else: + args = self.tgtArgs[:] + args.insert( 0, np ) + tgt = self.TgtFn( *args ) + + return tgt + + def Get( self, np ): + args = self.getArgs[:] + args.insert( 0, self.GetTarget( np ) ) + return self.GetFn( *args ) + + def Set( self, np, val ): + args = self.setArgs[:] + args.insert( 0, self.GetTarget( np ) ) + args.append( val ) + return self.SetFn( *args ) + + +class NodeAttribute( Attribute ): + + def GetTarget( self, np ): + return np.node() + + +class NodePathAttribute( Attribute ): + + def GetTarget( self, np ): + return np + + +class NodePathObjectAttribute( Attribute ): + + def __init__( self, label, pType, name ): + Attribute.__init__( self, label, pType, getattr, setattr, None, [name], [name], None ) + + def GetTarget( self, np ): + return p3d.NodePathObject.Get( np ) \ No newline at end of file diff --git a/src/pandaEditor/game/nodes/base.py b/src/pandaEditor/game/nodes/base.py new file mode 100644 index 0000000..d49cff4 --- /dev/null +++ b/src/pandaEditor/game/nodes/base.py @@ -0,0 +1,51 @@ +class Base( object ): + + def __init__( self, data=None ): + self.attributes = [] + self.children = [] + self.createArgs = {} + + if data is not None: + self.Wrap( data ) + + def Create( self, parent, args ): + pass + + def Duplicate( self, np, dupeNp ): + pass + + def Destroy( self ): + pass + + def GetAttributes( self ): + return self.attributes[:] + + def FindAttribute( self, name ): + for attr in self.attributes: + if attr.name == name: + return attr + + def GetData( self ): + dataDict = {} + + # Put this component's attributes into key / value pairs. + for attr in self.attributes: + if attr.w and attr.GetFn is not None: + dataDict[attr.name] = attr.Get( self.data ) + + return dataDict + + def SetData( self, dataDict ): + for key, value in dataDict.items(): + attr = self.FindAttribute( key ) + if attr is not None and attr.SetFn is not None: + attr.Set( self.data, value ) + else: + print 'Failed to load attribute: ', key + + def Wrap( self, data ): + self.data = data + + def GetChildWrapper( self, name ): + return None + \ No newline at end of file diff --git a/src/pandaEditor/game/nodes/baseCam.py b/src/pandaEditor/game/nodes/baseCam.py new file mode 100644 index 0000000..32aad6b --- /dev/null +++ b/src/pandaEditor/game/nodes/baseCam.py @@ -0,0 +1,9 @@ +from camera import Camera + + +class BaseCam( Camera ): + + def Create( self ): + self.SetupNodePath( base.cam ) + self.Wrap( base.cam ) + return base.cam \ No newline at end of file diff --git a/src/pandaEditor/game/nodes/baseCamera.py b/src/pandaEditor/game/nodes/baseCamera.py new file mode 100644 index 0000000..833d0f8 --- /dev/null +++ b/src/pandaEditor/game/nodes/baseCamera.py @@ -0,0 +1,9 @@ +from modelNode import ModelNode + + +class BaseCamera( ModelNode ): + + def Create( self ): + self.SetupNodePath( base.camera ) + self.Wrap( base.camera ) + return base.camera \ No newline at end of file diff --git a/src/pandaEditor/game/nodes/camera.py b/src/pandaEditor/game/nodes/camera.py new file mode 100644 index 0000000..500a4dc --- /dev/null +++ b/src/pandaEditor/game/nodes/camera.py @@ -0,0 +1,10 @@ +import pandac.PandaModules as pm + +from lensNode import LensNode + + +class Camera( LensNode ): + + def __init__( self, *args, **kwargs ): + kwargs['nType'] = pm.Camera + LensNode.__init__( self, *args, **kwargs ) \ No newline at end of file diff --git a/src/pandaEditor/game/nodes/collisionNode.py b/src/pandaEditor/game/nodes/collisionNode.py new file mode 100644 index 0000000..90897d4 --- /dev/null +++ b/src/pandaEditor/game/nodes/collisionNode.py @@ -0,0 +1,18 @@ +import pandac.PandaModules as pm +from pandac.PandaModules import CollisionNode as CN + +from nodePath import NodePath +from attributes import NodeAttribute as Attr + + +class CollisionNode( NodePath ): + + def __init__( self, *args, **kwargs ): + kwargs['nType'] = CN + NodePath.__init__( self, *args, **kwargs ) + + self.attributes.extend( + [ + Attr( 'Num Solids', int, CN.getNumSolids ) + ] + ) \ No newline at end of file diff --git a/src/pandaEditor/game/nodes/constants.py b/src/pandaEditor/game/nodes/constants.py new file mode 100644 index 0000000..dc4360d --- /dev/null +++ b/src/pandaEditor/game/nodes/constants.py @@ -0,0 +1,3 @@ +TAG_NODE_TYPE = 'type' +TAG_NODE_UUID = 'P3D_UUID' +TAG_PYTHON_TAGS = 'P3D_PythonTags' \ No newline at end of file diff --git a/src/pandaEditor/game/nodes/directionalLight.py b/src/pandaEditor/game/nodes/directionalLight.py new file mode 100644 index 0000000..932b9af --- /dev/null +++ b/src/pandaEditor/game/nodes/directionalLight.py @@ -0,0 +1,21 @@ +import pandac.PandaModules as pm +from pandac.PandaModules import DirectionalLight as DL + +from light import Light +from attributes import NodeAttribute as Attr + + +class DirectionalLight( Light ): + + def __init__( self, *args, **kwargs ): + kwargs['nType'] = DL + Light.__init__( self, *args, **kwargs ) + + self.attributes.extend( + [ + Attr( 'Direction', pm.Vec3, DL.getDirection, DL.setDirection ), + Attr( 'Point',pm.Point3, DL.getPoint, DL.setPoint ), + Attr( 'Specular Color', pm.Vec4, DL.getSpecularColor, DL.setSpecularColor ), + Attr( 'Shadow Caster', bool, DL.isShadowCaster, DL.setShadowCaster ) + ] + ) \ No newline at end of file diff --git a/src/pandaEditor/game/nodes/lensNode.py b/src/pandaEditor/game/nodes/lensNode.py new file mode 100644 index 0000000..1defc1a --- /dev/null +++ b/src/pandaEditor/game/nodes/lensNode.py @@ -0,0 +1,21 @@ +import pandac.PandaModules as pm +from pandac.PandaModules import Lens + +from nodePath import NodePath +from attributes import Attribute as Attr + + +class LensNode( NodePath ): + + def __init__( self, *args, **kwargs ): + kwargs['nType'] = pm.LensNode + NodePath.__init__( self, *args, **kwargs ) + + self.attributes.extend( + [ + Attr( 'Fov', pm.Vec2, Lens.getFov, Lens.setFov, self.GetLens ) + ] + ) + + def GetLens( self, np ): + return np.node().getLens() \ No newline at end of file diff --git a/src/pandaEditor/game/nodes/light.py b/src/pandaEditor/game/nodes/light.py new file mode 100644 index 0000000..ed61832 --- /dev/null +++ b/src/pandaEditor/game/nodes/light.py @@ -0,0 +1,25 @@ +import pandac.PandaModules as pm +from pandac.PandaModules import Light as L + +from nodePath import NodePath +from attributes import NodeAttribute as Attr + + +class Light( NodePath ): + + def __init__( self, *args, **kwargs ): + NodePath.__init__( self, *args, **kwargs ) + + self.attributes.extend( + [ + Attr( 'Color', pm.Vec4, L.getColor, L.setColor ) + ] + ) + + def Create( self ): + np = NodePath.Create( self ) + + # DEBUG + render.setLight( np ) + + return np \ No newline at end of file diff --git a/src/pandaEditor/game/nodes/manager.py b/src/pandaEditor/game/nodes/manager.py new file mode 100644 index 0000000..da34046 --- /dev/null +++ b/src/pandaEditor/game/nodes/manager.py @@ -0,0 +1,74 @@ +from constants import * + + +class Manager( object ): + + def __init__( self ): + from nodePath import NodePath + from actor import Actor + from pandaNode import PandaNode + from collisionNode import CollisionNode + from camera import Camera + from baseCam import BaseCam + from modelNode import ModelNode + from baseCamera import BaseCamera + from modelRoot import ModelRoot + + from light import Light + from ambientLight import AmbientLight + from pointLight import PointLight + from directionalLight import DirectionalLight + from spotlight import Spotlight + + self.nodeWrappers = { + 'NodePath':NodePath, + 'PandaNode':PandaNode, + 'Actor':Actor, + 'CollisionNode':CollisionNode, + 'Camera':Camera, + 'BaseCam':BaseCam, + 'ModelNode':ModelNode, + 'BaseCamera':BaseCamera, + 'ModelRoot':ModelRoot, + + 'Light':Light, + 'AmbientLight':AmbientLight, + 'PointLight':PointLight, + 'DirectionalLight':DirectionalLight, + 'Spotlight':Spotlight + } + + self.pyTagWrappers = {} + + def Create( self, nTypeStr, *args ): + Wrpr = self.nodeWrappers[nTypeStr] + wrpr = Wrpr() + return wrpr.Create( *args ) + + def Wrap( self, np ): + Wrpr = self.GetWrapper( np ) + if Wrpr is not None: + return Wrpr( np ) + + return None + + def GetWrapper( self, np ): + typeStr = self.GetTypeString( np ) + if typeStr in self.nodeWrappers: + return self.nodeWrappers[typeStr] + + return None + + def GetWrapperByName( self, nTypeStr ): + if nTypeStr in self.nodeWrappers: + return self.nodeWrappers[nTypeStr] + + return None + + def GetTypeString( self, np ): + """Return the type of the NodePath.""" + nType = np.node().getTag( TAG_NODE_TYPE ) + if not nType: + nType = type( np.node() ).__name__ + + return nType \ No newline at end of file diff --git a/src/pandaEditor/game/nodes/modelNode.py b/src/pandaEditor/game/nodes/modelNode.py new file mode 100644 index 0000000..36dedec --- /dev/null +++ b/src/pandaEditor/game/nodes/modelNode.py @@ -0,0 +1,10 @@ +import pandac.PandaModules as pm + +from nodePath import NodePath + + +class ModelNode( NodePath ): + + def __init__( self, *args, **kwargs ): + kwargs['nType'] = pm.ModelNode + NodePath.__init__( self, *args, **kwargs ) \ No newline at end of file diff --git a/src/pandaEditor/game/nodes/modelRoot.py b/src/pandaEditor/game/nodes/modelRoot.py new file mode 100644 index 0000000..be9c733 --- /dev/null +++ b/src/pandaEditor/game/nodes/modelRoot.py @@ -0,0 +1,73 @@ +import os + +import pandac.PandaModules as pm + +from constants import * +from nodePath import NodePath +from attributes import NodeAttribute as Attr + + +class ModelRoot( NodePath ): + + def __init__( self, *args, **kwargs ): + kwargs['nType'] = pm.ModelRoot + NodePath.__init__( self, *args, **kwargs ) + + def Create( self, modelPath='' ): + filePath = pm.Filename.fromOsSpecific( modelPath ) + try: + np = loader.loadModel( filePath ) + except: + try: + np = loader.loadModel( filePath + '.bam' ) + except IOError: + print 'Failed to load: ', filePath + np = pm.NodePath( pm.ModelRoot( '' ) ) + np.setName( filePath.getBasenameWoExtension() ) + + # Iterate over child nodes + def Recurse( node ): + nTypeStr = node.getTag( TAG_NODE_TYPE ) + Wrpr = base.game.nodeMgr.GetWrapperByName( nTypeStr ) + if Wrpr is not None: + wrpr = Wrpr( node ) + wrpr.Create( inputNp=node ) + + # Recurse + for child in node.getChildren(): + Recurse( child ) + + Recurse( np ) + + self.SetupNodePath( np ) + self.Wrap( np ) + + return np + + def Wrap( self, np ): + NodePath.Wrap( self, np ) + + self.createArgs = {'modelPath':self.GetRelModelPath()} + + def GetRelModelPath( self ): + """ + Attempt to find the indicated file path on one of the model search + paths. If found then return a path relative to it. Also make sure to + remove all extensions so we can load both egg and bam files. + """ + node = self.data.node() + + relPath = pm.Filename( node.getFullpath() ) + index = relPath.findOnSearchpath( pm.getModelPath().getValue() ) + if index >= 0: + basePath = pm.getModelPath().getDirectories()[index] + relPath.makeRelativeTo( basePath ) + + # Remove all extensions + modelPath = str( relPath ) + while True: + modelPath, ext = os.path.splitext( modelPath ) + if not ext: + break + + return modelPath \ No newline at end of file diff --git a/src/pandaEditor/game/nodes/nodePath.py b/src/pandaEditor/game/nodes/nodePath.py new file mode 100644 index 0000000..0278486 --- /dev/null +++ b/src/pandaEditor/game/nodes/nodePath.py @@ -0,0 +1,91 @@ +import uuid + +import pandac.PandaModules as pm +from pandac.PandaModules import NodePath as NP + +from base import Base +from constants import * +from attributes import NodePathAttribute as Attr + + +class NodePath( Base ): + + def __init__( self, *args, **kwargs ): + nType = kwargs.pop( 'nType', None ) + Base.__init__( self, *args, **kwargs ) + + self.name = 'Node' + + # Generate a default name from the node type. + self.type = nType + if self.type is not None: + nodeName = self.type.__name__ + self.nodeName = nodeName[0:1].lower() + nodeName[1:] + + self.attributes.extend( + [ + Attr( 'Name', str, NP.getName, NP.setName ), + Attr( 'Matrix', pm.Mat4, NP.getMat, NP.setMat ), + Attr( 'Uuid', str, NP.getTag, NP.setTag, None, [TAG_NODE_UUID], [TAG_NODE_UUID], e=False ) + ] + ) + + def SetupNodePath( self, np ): + id = str( uuid.uuid4() ) + np.setTag( TAG_NODE_UUID, id ) + + def Create( self ): + """ + Create a NodePath with the indicated type and name, set it up and + return it. + """ + np = pm.NodePath( self.type( self.nodeName ) ) + self.SetupNodePath( np ) + self.Wrap( np ) + return pm.NodePath( np ) + + def Duplicate( self, np, dupeNp ): + Base.Duplicate( self, np, dupeNp ) + + for child in self.children: + child.Duplicate( np, dupeNp ) + base.game.pluginMgr.OnNodeDuplicate( self.data ) + + def Destroy( self ): + Base.Destroy( self ) + + for child in self.children: + child.Destroy() + base.game.pluginMgr.OnNodeDestroy( self.data ) + + def GetAttributes( self ): + attrs = Base.GetAttributes( self ) + for child in self.children: + attrs.extend( child.GetAttributes() ) + + return attrs + + def GetChildWrapper( self, name ): + + if name in base.game.nodeMgr.pyTagWrappers: + return base.game.nodeMgr.pyTagWrappers[name] + + return None + + def GetTags( self ): + tags = self.data.getPythonTag( TAG_PYTHON_TAGS ) + if tags is not None: + return [tag for tag in tags if tag in base.game.nodeMgr.pyTagWrappers] + + return [] + + def Wrap( self, np ): + Base.Wrap( self, np ) + + # Wrap python objects + tags = self.GetTags() + for tag in tags: + pyObj = np.getPythonTag( tag ) + pyObjWrpr = base.game.nodeMgr.pyTagWrappers[tag] + wrpr = pyObjWrpr( pyObj ) + self.children.append( wrpr ) \ No newline at end of file diff --git a/src/pandaEditor/game/nodes/pandaNode.py b/src/pandaEditor/game/nodes/pandaNode.py new file mode 100644 index 0000000..2b2ba5e --- /dev/null +++ b/src/pandaEditor/game/nodes/pandaNode.py @@ -0,0 +1,10 @@ +import pandac.PandaModules as pm + +from nodePath import NodePath + + +class PandaNode( NodePath ): + + def __init__( self, *args, **kwargs ): + kwargs['nType'] = pm.PandaNode + NodePath.__init__( self, *args, **kwargs ) \ No newline at end of file diff --git a/src/pandaEditor/game/nodes/pointLight.py b/src/pandaEditor/game/nodes/pointLight.py new file mode 100644 index 0000000..6a68974 --- /dev/null +++ b/src/pandaEditor/game/nodes/pointLight.py @@ -0,0 +1,20 @@ +import pandac.PandaModules as pm +from pandac.PandaModules import PointLight as PL + +from light import Light +from attributes import NodeAttribute as Attr + + +class PointLight( Light ): + + def __init__( self, *args, **kwargs ): + kwargs['nType'] = PL + Light.__init__( self, *args, **kwargs ) + + self.attributes.extend( + [ + Attr( 'Attenuation', pm.Vec3, PL.getAttenuation, PL.setAttenuation ), + Attr( 'Point', pm.Vec3, PL.getPoint, PL.setPoint ), + Attr( 'Specular Color', pm.Vec4, PL.getSpecularColor, PL.setSpecularColor ) + ] + ) \ No newline at end of file diff --git a/src/pandaEditor/game/nodes/spotlight.py b/src/pandaEditor/game/nodes/spotlight.py new file mode 100644 index 0000000..801159a --- /dev/null +++ b/src/pandaEditor/game/nodes/spotlight.py @@ -0,0 +1,23 @@ +import pandac.PandaModules as pm +from pandac.PandaModules import Spotlight as SL + +from light import Light +from lensNode import LensNode +from attributes import NodeAttribute as Attr + + +class Spotlight( Light, LensNode ): + + def __init__( self, *args, **kwargs ): + kwargs['nType'] = SL + LensNode.__init__( self, *args, **kwargs ) + Light.__init__( self, *args, **kwargs ) + + self.attributes.extend( + [ + Attr( 'Attenuation', pm.Vec3, SL.getAttenuation, SL.setAttenuation ), + Attr( 'Exponent', float, SL.getExponent, SL.setExponent ), + Attr( 'Specular Color', pm.Vec4, SL.getSpecularColor, SL.setSpecularColor ), + Attr( 'Shadow Caster', bool, SL.isShadowCaster, SL.setShadowCaster ) + ] + ) \ No newline at end of file diff --git a/src/pandaEditor/game/plugins/__init__.py b/src/pandaEditor/game/plugins/__init__.py new file mode 100644 index 0000000..634ebe2 --- /dev/null +++ b/src/pandaEditor/game/plugins/__init__.py @@ -0,0 +1 @@ +from manager import Manager \ No newline at end of file diff --git a/src/pandaEditor/game/plugins/base.py b/src/pandaEditor/game/plugins/base.py new file mode 100644 index 0000000..083bc80 --- /dev/null +++ b/src/pandaEditor/game/plugins/base.py @@ -0,0 +1,22 @@ +class Base( object ): + + def __init__( self, game, name='', sort=0, priority=10 ): + self.game = game + self.name = name + self._sort = sort + self._priority = priority + + def OnInit( self ): + pass + + def RegisterNodeWrapper( self, typeStr, cls ): + self.game.nodeMgr.nodeWrappers[typeStr] = cls + + def RegisterPyTagWrapper( self, typeStr, cls ): + self.game.nodeMgr.pyTagWrappers[typeStr] = cls + + def OnNodeDuplicate( self, np ): + pass + + def OnNodeDestroy( self, np ): + pass \ No newline at end of file diff --git a/src/pandaEditor/game/plugins/manager.py b/src/pandaEditor/game/plugins/manager.py new file mode 100644 index 0000000..50286cc --- /dev/null +++ b/src/pandaEditor/game/plugins/manager.py @@ -0,0 +1,69 @@ +import os +import sys +import traceback + + +class Manager( object ): + + def __init__( self, game ): + self.game = game + + self.plugins = [] + + def LoadPlugin( self, fileName ): + try: + mod = __import__( fileName ) + cls = getattr( mod.gamePlugin, 'GamePlugin' ) + return cls( self.game ) + except Exception, e: + traceback.print_exc() + return None + + def GetPluginsPath( self ): + """ + Attempt to import plugins directory. Return None if it wasn't found. + """ + try: + import userPlugins + except ImportError: + print 'Failed to load plugins.' + return None + + return os.path.split( userPlugins.__file__ )[0] + + def Load( self ): + """Attempt to load plugins from their directory.""" + # Put the plugins directory on sys.path. + pluginsPath = self.GetPluginsPath() + if pluginsPath is None: + return + + if pluginsPath not in sys.path: + sys.path.insert( 0, pluginsPath ) + + print 'Using plugins path: ', pluginsPath + + # Load all plugins + for fileName in os.listdir( pluginsPath ): + filePath = os.path.join( pluginsPath, fileName ) + if os.path.isdir( filePath ): + plugin = self.LoadPlugin( fileName ) + if plugin is not None: + self.plugins.append( plugin ) + + # Now run their OnInit methods + self.SortPlugins() + for plugin in self.plugins: + plugin.OnInit() + + def SortPlugins( self ): + """Sort plugins by accending sort order.""" + self.plugins = sorted( self.plugins, key=lambda plugin: plugin._sort ) + + def OnNodeDuplicate( self, np ): + for plugin in self.plugins: + plugin.OnNodeDuplicate( np ) + + def OnNodeDestroy( self, np ): + for plugin in self.plugins: + plugin.OnNodeDestroy( np ) \ No newline at end of file diff --git a/src/pandaEditor/game/sceneParser.py b/src/pandaEditor/game/sceneParser.py new file mode 100644 index 0000000..a3cdd18 --- /dev/null +++ b/src/pandaEditor/game/sceneParser.py @@ -0,0 +1,88 @@ +import xml.etree.cElementTree as et + +import p3d + + +class SceneParser( object ): + + """A class to load map files into Panda3D.""" + + def __init__( self ): + self.loadCastFnMap = { + 'bool':p3d.Str2Bool, + 'float':float, + 'int':int, + 'str':str, + 'LVector2f':p3d.Str2FloatTuple, + 'LVecBase2f':p3d.Str2FloatTuple, + 'LVector3f':p3d.Str2FloatTuple, + 'LVecBase3f':p3d.Str2FloatTuple, + 'LVector4f':p3d.Str2FloatTuple, + 'LVecBase4f':p3d.Str2FloatTuple, + 'LPoint3f':p3d.Str2FloatTuple, + 'LMatrix4f':p3d.Str2Mat4 + } + + def GetCreateArgs( self, attrib ): + attrib.pop( 'type' ) + return attrib + + def LoadData( self, wrpr, elem ): + + # Pull all data from the xml for this component, then get the wrapper + # to set the data. + dataElems = elem.findall( 'Item' ) + dataDict = {} + for dataElem in dataElems: + dataType = dataElem.get( 'type' ) + if dataType in self.loadCastFnMap: + castFn = self.loadCastFnMap[dataType] + dataDict[dataElem.get( 'name' )] = castFn( dataElem.get( 'value' ) ) + else: + print 'Could not load attribute: ', dataElem.get( 'name' ), ' : of type: ', dataType + wrpr.SetData( dataDict ) + + # Do child components + cElems = elem.findall( 'Component' ) + for cElem in cElems: + CWrpr = wrpr.GetChildWrapper( cElem.get( 'type' ) ) + if CWrpr is not None: + cWrpr = CWrpr() + cWrpr.Create( wrpr.data, **self.GetCreateArgs( cElem.attrib ) ) + self.LoadData( cWrpr, cElem ) + + def LoadNode( self, elem, parentNp ): + """Build an element from xml.""" + # Build the node + strType = elem.get( 'type' ) + if elem.tag == 'Component' and strType in base.game.nodeMgr.nodeWrappers: + + # Get the node type + Wrpr = base.game.nodeMgr.GetWrapperByName( strType ) + if Wrpr is not None: + + # Create the base node path + wrpr = Wrpr() + np = wrpr.Create( **self.GetCreateArgs( elem.attrib ) ) + np.reparentTo( parentNp ) + + # Load data for the node. + self.LoadData( wrpr, elem ) + + # At this stage np might be an actor, so cast back to node path. + # This may cause some weird issues. Not sure... + np = np.anyPath( np.node() ) + + else: + np = parentNp + + # Recurse down the tree + cElem = elem.find( 'Children' ) + if cElem is not None: + for childElem in cElem: + self.LoadNode( childElem, np ) + + def Load( self, rootNp, filePath ): + """Load the scene from an xml file.""" + tree = et.parse( filePath ) + self.LoadNode( tree.getroot(), rootNp ) \ No newline at end of file diff --git a/src/pandaEditor/gizmos/__init__.py b/src/pandaEditor/gizmos/__init__.py new file mode 100644 index 0000000..7640691 --- /dev/null +++ b/src/pandaEditor/gizmos/__init__.py @@ -0,0 +1,8 @@ +from constants import * + +from manager import Manager +from axis import Axis +from base import Base +from translation import Translation +from rotation import Rotation +from scale import Scale \ No newline at end of file diff --git a/src/pandaEditor/gizmos/axis.py b/src/pandaEditor/gizmos/axis.py new file mode 100644 index 0000000..09e3b84 --- /dev/null +++ b/src/pandaEditor/gizmos/axis.py @@ -0,0 +1,130 @@ +from pandac.PandaModules import Point3, NodePath, GeomEnums +from pandac.PandaModules import CollisionNode, CollisionTube + +from constants import * + + +class Axis( NodePath ): + + def __init__( self, name, vector, colour, planar=False, default=False ): + NodePath.__init__( self, name ) + + self.name = name + self.vector = vector + self.colour = colour + self.planar = planar + self.default = default + + self.highlited = False + self.selected = False + self.size = 1 + + self.geoms = [] + self.collNodes = [] + self.collNodePaths = [] + + def AddGeometry( self, geom, pos=Point3(0, 0, 0), colour=None, + highlight=True, sizeStyle=TRANSLATE ): + """ + Add geometry to represent the axis and move it into position. If the + geometry is a line make sure to call setLightOff or else it won't + look right. + """ + geom.setPos( pos ) + geom.setPythonTag( 'highlight', highlight ) + geom.setPythonTag( 'sizeStyle', sizeStyle ) + geom.reparentTo( self ) + + # If colour is not specified then use the axis colour + if colour is None: + colour = self.colour + geom.setColorScale( colour ) + + # Set light off if the geometry is a line + if geom.node().getGeom( 0 ).getPrimitiveType() == GeomEnums.PTLines: + geom.setLightOff() + + self.geoms.append( geom ) + + def AddCollisionSolid( self, collSolid, pos=Point3(0, 0, 0), + sizeStyle=TRANSLATE ): + """Add a collision solid to the axis and move it into position.""" + # Create the collision node and add the solid + collNode = CollisionNode( self.name ) + collNode.addSolid( collSolid ) + self.collNodes.append( collNode ) + + # Create a node path and move it into position + collNodePath = self.attachNewNode( collNode ) + collNodePath.setPos( pos ) + collNodePath.setPythonTag( 'sizeStyle', sizeStyle ) + self.collNodePaths.append( collNodePath ) + + def SetSize( self, size ): + """ + Change the size of the gizmo. This isn't just the same as scaling all + the geometry and collision - sometimes this just means pushing the + geometry along the axis instead. + """ + oldSize = self.size + self.size = size + + nodePaths = self.geoms + self.collNodePaths + for nodePath in nodePaths: + + # Get the size style + sizeStyle = nodePath.getPythonTag( 'sizeStyle' ) + if sizeStyle & NONE: + continue + + # Set scale + if sizeStyle & SCALE: + nodePath.setScale( self.size ) + + # Set position + if sizeStyle & TRANSLATE: + + # Get the position of the node path relative to the axis end + # point (vector), then move the geometry and reapply this + # offset + diff = ( self.vector * oldSize ) - nodePath.getPos() + nodePath.setPos( Point3( ( self.vector * self.size ) - diff ) ) + + # Should only be used for collision tubes + if sizeStyle & TRANSLATE_POINT_B: + collSolid = nodePath.node().modifySolid( 0 ) + if type( collSolid ) == CollisionTube: + + # Get the position of the capsule's B point relative to + # the axis end point (vector), then move the point and + # reapply this offset + diff = ( self.vector * oldSize ) - collSolid.getPointB() + collSolid.setPointB( Point3( ( self.vector * self.size ) - diff ) ) + + def Select( self ): + """ + Changed the colour of the axis to the highlight colour and flag as + being selected. + """ + self.selected = True + self.Highlight() + + def Deselect( self ): + """ + Reset the colour of the axis to the original colour and flag as being + unselected. + """ + self.selected = False + self.Unhighlight() + + def Highlight( self ): + """Highlight the axis by changing it's colour.""" + for geom in self.geoms: + if geom.getPythonTag( 'highlight' ): + geom.setColorScale( YELLOW ) + + def Unhighlight( self ): + """Remove the highlight by resetting to the axis colour.""" + for geom in self.geoms: + if geom.getPythonTag( 'highlight' ): + geom.setColorScale( self.colour ) \ No newline at end of file diff --git a/src/pandaEditor/gizmos/base.py b/src/pandaEditor/gizmos/base.py new file mode 100644 index 0000000..5997c56 --- /dev/null +++ b/src/pandaEditor/gizmos/base.py @@ -0,0 +1,273 @@ +from pandac.PandaModules import Point3, Vec3, Plane, NodePath + +import p3d +from constants import * + + +class Base( NodePath, p3d.SingleTask ): + + def __init__( self, name, *args, **kwargs ): + NodePath.__init__( self, name ) + p3d.SingleTask.__init__( self, name, *args, **kwargs ) + + self.attachedNps = [] + self.dragging = False + self.local = True + self.planar = False + self.size = 1 + + self.axes = [] + + # Set this node up to be drawn over everything else + self.setBin( 'fixed', 40 ) + self.setDepthTest( False ) + self.setDepthWrite( False ) + + def OnUpdate( self, task ): + """ + Main update task used by the gizmo which is added and removed from the + task manager whenever a gizmo is stopped and started. + """ + # Increate gizmo side by distance to camera so it always appears the + # same size + scale = ( self.getPos() - self.camera.getPos() ).length() / 10 + self.setScale( scale ) + + # Call transform if the mouse is down + if self.dragging: + self.Transform() + + def OnStart( self ): + """ + Starts the gizmo adding the task to the task manager, refreshing it + and deselecting all axes except the default one. + """ + # Refresh the gizmo + self.Refresh() + + # Select the default axis, deselect all others + for axis in self.axes: + if axis.default: + axis.Select() + else: + axis.Deselect() + + # Accept events + self.AcceptEvents() + + def OnStop( self ): + """ + Stops the gizmo by hiding it and removing it's update task from the + task manager. + """ + # Hide the gizmo and ignore all events + self.detachNode() + self.ignoreAll() + + def AcceptEvents( self ): + """Bind all events for the gizmo.""" + self.accept( 'mouse1-up', self.OnMouseUp ) + self.accept( 'mouse2-up', self.OnMouseUp ) + self.accept( 'mouse2', self.OnMouse2Down ) + self.accept( ''.join( [self.name, '-mouse1'] ), self.OnNodeMouse1Down, [False] ) + self.accept( ''.join( [self.name, '-control-mouse1'] ), self.OnNodeMouse1Down, [True] ) + self.accept( ''.join( [self.name, '-mouse-over'] ), self.OnNodeMouseOver ) + self.accept( ''.join( [self.name, '-mouse-leave'] ), self.OnNodeMouseLeave ) + + def Transform( self ): + """ + Override this method to provide the gizmo with transform behavior. + """ + pass + + def AttachNodePaths( self, nps ): + """ + Attach node paths to the gizmo. This won't affect the node's position + in the scene graph, but will transform the objects with the gizmo. + """ + self.attachedNps = nps + + def SetSize( self, factor ): + """ + Used to scale the gizmo by a factor, usually by 2 (scale up) and 0.5 + (scale down). Set both the new size for the gizmo also call set size + on all axes. + """ + self.size *= factor + + # Each axis may have different rules on how to appear when scaled, so + # call set size on each of them + for axis in self.axes: + axis.SetSize( self.size ) + + def GetAxis( self, collEntry ): + """ + Iterate over all axes of the gizmo, return the axis that owns the + solid responsible for the collision. + """ + for axis in self.axes: + if collEntry.getIntoNode() in axis.collNodes: + return axis + + # No match found, return None + return None + + def GetSelectedAxis( self ): + """Return the selected axis of the gizmo.""" + for axis in self.axes: + if axis.selected: + return axis + + def ResetAxes( self ): + """ + Reset the default colours and flag as unselected for all axes in the + gizmo. + """ + for axis in self.axes: + axis.Deselect() + + def Refresh( self ): + """ + If the gizmo has node paths attached to it then move the gizmo into + position, set its orientation and show it. Otherwise hide the gizmo. + """ + if self.attachedNps: + + # Show the gizmo + self.reparentTo( self.rootNp ) + + # Move the gizmo into position + self.setPos( self.attachedNps[0].getPos( self.rootNp ) ) + + # Only set the orientation of the gizmo if in local mode + if self.local: + self.setHpr( self.attachedNps[0].getHpr( self.rootNp ) ) + else: + self.setHpr( self.rootNp.getHpr() ) + + else: + + # Hide the gizmo + self.detachNode() + + def OnMouseUp( self ): + """ + Set the dragging flag to false and reset the size of the gizmo on the + mouse button is released. + """ + self.dragging = False + self.SetSize( 1 ) + + def OnNodeMouseLeave( self, collEntry ): + """ + Called when the mouse leaves the the collision object. Remove the + highlight from any axes which aren't selected. + """ + for axis in self.axes: + if not axis.selected: + axis.Unhighlight() + + def OnNodeMouse1Down( self, planar, collEntry ): + self.planar = planar + self.dragging = True + + # Store the attached node path's transforms + self.initNpXforms = [np.getTransform() for np in self.attachedNps] + + # Reset colours and deselect all axes, then get the one which the + # mouse is over + self.ResetAxes() + axis = self.GetAxis( collEntry ) + if axis is not None: + + # Select it + axis.Select() + + # Get the initial point where the mouse clicked the axis + self.initMousePoint = self.GetAxisPoint( axis ) + + def OnMouse2Down( self ): + """ + Continue transform operation if user is holding mouse2 but not over + the gizmo. + """ + axis = self.GetSelectedAxis() + if axis is not None and self.attachedNps and self.mouseWatcherNode.hasMouse(): + self.dragging = True + self.initNpXforms = [np.getTransform() for np in self.attachedNps] + self.initMousePoint = self.GetAxisPoint( axis ) + + def OnNodeMouseOver( self, collEntry ): + """Highlights the different axes as the mouse passes over them.""" + # Don't change highlighting if in dragging mode + if self.dragging: + return + + # Remove highlight from all unselected axes + for axis in self.axes: + if not axis.selected: + axis.Unhighlight() + + # Highlight the axis which the mouse is over + axis = self.GetAxis( collEntry ) + if axis is not None: + axis.Highlight() + + def GetAxisPoint( self, axis ): + + def GetMousePlaneCollisionPoint( planeNormal ): + """ + Return the collision point of a ray fired through the mouse and a + plane with the specified normal. + """ + # Fire a ray from the camera through the mouse + mp = self.mouseWatcherNode.getMouse() + p1 = Point3() + p2 = Point3() + self.camera.node().getLens().extrude( mp, p1, p2 ) + p1 = render.getRelativePoint( self.camera, p1 ) + p2 = render.getRelativePoint( self.camera, p2 ) + + # Get the point of intersection with a plane with the normal + # specified + p = Point3() + Plane( planeNormal, self.getPos() ).intersectsLine( p, p1, p2 ) + + return p + + def ClosestPointToLine( c, a, b ): + + """Returns the closest point on line ab to input point c.""" + + u = ( c[0] - a[0] ) * ( b[0] - a[0] ) + ( c[1] - a[1] ) * ( b[1] - a[1] ) + ( c[2] - a[2] ) * ( b[2] - a[2] ) + u = u / ( ( a - b ).length() * ( a - b ).length() ) + + x = a[0] + u * ( b[0] - a[0] ) + y = a[1] + u * ( b[1] - a[1] ) + z = a[2] + u * ( b[2] - a[2] ) + + return Point3(x, y, z) + + # Get the axis vector - by default this is the selected axis' + # vector unless we need to use the camera's look vector + if axis.vector == CAMERA_VECTOR: + axisVector = render.getRelativeVector( self.camera, Vec3(0, -1, 0) ) + else: + axisVector = render.getRelativeVector( self, axis.vector ) + + # Get the transform plane's normal. If we're transforming in + # planar mode use the axis vector as the plane normal, otherwise + # get the normal of a plane along the selected axis + if self.planar or axis.planar: + return GetMousePlaneCollisionPoint( axisVector ) + else: + + # Get the cross of the camera vector and the axis vector - a + # vector of 0, 1, 0 in camera space is coming out of the lens + camVector = render.getRelativeVector( self.camera, Vec3(0, 1, 0) ) + camAxisCross = camVector.cross( axisVector ) + + # Cross this back with the axis to get a plane's normal + planeNormal = camAxisCross.cross( axisVector ) + p = GetMousePlaneCollisionPoint( planeNormal ) + return ClosestPointToLine( p, self.getPos(), self.getPos() + axisVector ) \ No newline at end of file diff --git a/src/pandaEditor/gizmos/constants.py b/src/pandaEditor/gizmos/constants.py new file mode 100644 index 0000000..f14dc9f --- /dev/null +++ b/src/pandaEditor/gizmos/constants.py @@ -0,0 +1,13 @@ +RED = (1, 0, 0, 0) +GREEN = (0, 1, 0, 0) +BLUE = (0, 0, 1, 0) +YELLOW = (1, 1, 0, 0) +TEAL = (0, 1, 1, 0) +GREY = (0.5, 0.5, 0.5, 0.5) + +TRANSLATE = 1 +TRANSLATE_POINT_A = 2 +TRANSLATE_POINT_B = 4 +SCALE = 8 +NONE = 16 +CAMERA_VECTOR = 32 \ No newline at end of file diff --git a/src/pandaEditor/gizmos/manager.py b/src/pandaEditor/gizmos/manager.py new file mode 100644 index 0000000..e522619 --- /dev/null +++ b/src/pandaEditor/gizmos/manager.py @@ -0,0 +1,104 @@ +from pandac.PandaModules import DirectionalLight + +import p3d + + +class Manager( p3d.Object ): + + def __init__( self, *args, **kwargs ): + p3d.Object.__init__( self, *args, **kwargs ) + + self._gizmos = {} + self._activeGizmo = None + + # Create gizmo manager mouse picker + self.picker = p3d.MousePicker( 'mouse', *args, **kwargs ) + self.picker.Start() + + # Create a directional light and attach it to the camera so the gizmos + # don't look flat + dl = DirectionalLight( 'gizmoManagerDirLight' ) + self.dlNp = self.camera.attachNewNode( dl ) + self.rootNp.setLight( self.dlNp ) + + def AddGizmo( self, gizmo ): + """Add a gizmo to be managed by the gizmo manager.""" + gizmo.rootNp = self.rootNp + self._gizmos[gizmo.getName()] = gizmo + + def GetGizmo( self, name ): + """ + Find and return a gizmo by name, return None if no gizmo with the + specified name exists. + """ + if name in self._gizmos: + return self._gizmos[name] + + return None + + def GetActiveGizmo( self ): + """Return the active gizmo.""" + return self._activeGizmo + + def SetActiveGizmo( self, name ): + """ + Stops the currently active gizmo then finds the specified gizmo by + name and starts it. + """ + # Stop the active gizmo + if self._activeGizmo is not None: + self._activeGizmo.Stop() + + # Get the gizmo by name and start it if it is a valid gizmo + self._activeGizmo = self.GetGizmo( name ) + if self._activeGizmo is not None: + self._activeGizmo.Start() + + def RefreshActiveGizmo( self ): + + # Get the active gizmo + activeGizmo = self.GetActiveGizmo() + if activeGizmo is not None: + + # Refresh the active gizmo so it appears in the right place + activeGizmo.Refresh() + + def GetGizmoLocal( self, name ): + """Return the gizmos local mode.""" + gizmo = self.GetGizmo( name ) + if gizmo is not None: + return gizmo.local + + def SetGizmoLocal( self, name, mode ): + """Set all gizmo local modes, then refresh the active one.""" + gizmo = self.GetGizmo( name ) + if gizmo is not None: + gizmo.local = mode + + if self._activeGizmo is not None: + self._activeGizmo.Refresh() + + def ToggleLocal( self ): + """Toggle all gizmos local mode on or off.""" + for gizmo in self._gizmos.values(): + gizmo.local = not gizmo.local + + if self._activeGizmo is not None: + self._activeGizmo.Refresh() + + def SetSize( self, factor ): + """Resize the gizmo by a factor.""" + for gizmo in self._gizmos.values(): + gizmo.SetSize( factor ) + + def AttachNodePaths( self, nps ): + """Attach node paths to be transformed by the gizmos.""" + for gizmo in self._gizmos.values(): + gizmo.AttachNodePaths( nps ) + + def IsDragging( self ): + """ + Return True if the active gizmo is in the middle of a dragging + operation, False otherwise. + """ + return self._activeGizmo is not None and self._activeGizmo.dragging \ No newline at end of file diff --git a/src/pandaEditor/gizmos/rotation.py b/src/pandaEditor/gizmos/rotation.py new file mode 100644 index 0000000..8583cf6 --- /dev/null +++ b/src/pandaEditor/gizmos/rotation.py @@ -0,0 +1,219 @@ +import math + +from pandac.PandaModules import Mat4, Vec3, Point3, Plane +from pandac.PandaModules import CollisionSphere, CollisionPolygon +from pandac.PandaModules import BillboardEffect + +from p3d import commonUtils +from p3d.geometry import Arc, Line +from axis import Axis +from base import Base +from constants import * + + +class Rotation( Base ): + + def __init__( self, *args, **kwargs ): + Base.__init__( self, *args, **kwargs ) + + # Create the camera helper + self.cameraHelper = self.rootNp.attachNewNode( 'cameraHelper' ) + + # Create the 'ball' border + self.border = self.CreateCircle( GREY, 1 ) + + # Create the collision sphere - except for the camera normal, all axes + # will use this single collision object + self.collSphere = CollisionSphere( 0, 1 ) + + # Create x, y, z and camera normal axes + self.axes.append( self.CreateRing( Vec3(1, 0, 0), RED, self.camera ) ) + self.axes.append( self.CreateRing( Vec3(0, 1, 0), GREEN, self.cameraHelper ) ) + self.axes.append( self.CreateRing( Vec3(0, 0, 1), BLUE, self.camera ) ) + + # DEBUG + self.foobar = self.CreateCamCircle( TEAL, 1.2 ) + self.axes.append( self.foobar ) + + def CreateRing( self, vector, colour, lookAt ): + + # Create the billboard effect + bbe = BillboardEffect.make( vector, False, True, 0, lookAt, (0, 0, 0) ) + + # Create an arc + arc = Arc( numSegs=32, degrees=180, axis=Vec3(0, 0, 1) ) + arc.setH( 180 ) + arc.setEffect( bbe ) + + # Create the axis from the arc + axis = Axis( self.name, vector, colour ) + axis.AddGeometry( arc, sizeStyle=SCALE ) + axis.AddCollisionSolid( self.collSphere, sizeStyle=SCALE ) + axis.reparentTo( self ) + + return axis + + def CreateCircle( self, colour, radius ): + + # Create a circle + arc = Arc( radius, numSegs=64, axis=Vec3(0, 1, 0) ) + arc.setColorScale( colour ) + arc.setLightOff() + arc.reparentTo( self ) + + # Set the billboard effect + arc.setBillboardPointEye() + + return arc + + def CreateCamCircle( self, colour, radius ): + + # Create the geometry and collision + circle = self.CreateCircle( colour, radius ) + collPoly = CollisionPolygon( Point3(-1.2, 0, -1.2), Point3(-1.25, 0, 1.25), Point3(1.25, 0, 1.25), Point3(1.25, 0, -1.25) ) + + # Create the axis, add the geometry and collision + self.camAxis = Axis( self.name, CAMERA_VECTOR, colour, planar=True, default=True ) + self.camAxis.AddGeometry( circle, sizeStyle=SCALE ) + self.camAxis.AddCollisionSolid( collPoly, sizeStyle=SCALE ) + self.camAxis.reparentTo( self ) + + return self.camAxis + + def SetSize( self, factor ): + Base.SetSize( self, factor ) + + # Scale up any additional geo + self.border.setScale( self.size ) + + def GetAxis( self, collEntry ): + axis = Base.GetAxis( self, collEntry ) + + # Return None if the axis is None + if axis is None: + return None + + if axis.vector != CAMERA_VECTOR: + + # Return the axis from the specified normal within a tolerance of + # degrees + normal = collEntry.getSurfaceNormal( self ) + normal.normalize() + for axis in self.axes: + if math.fabs( normal.angleDeg( axis.vector ) - 90 ) < ( 2.5 / self.size ): + return axis + else: + + # Get the collision point on the poly, return the axis if the + # mouse is within tolerance of the circle + point = collEntry.getSurfacePoint( collEntry.getIntoNodePath() ) + length = Vec3( point / 1.25 ).length() + if length > 0.9 and length < 1: + return axis + + def Update( self, task ): + Base.Update( self, task ) + + # Update the position of the camera helper based on the position of + # the camera + finalMat = self.camera.getMat( self ) * Mat4().scaleMat( 1, 1, -1 ) + self.cameraHelper.setMat( self, finalMat ) + + # DEBUG - make the camera normal collision plane look at the camera. + # Probably should be a better way to do this. + self.camAxis.collNodePaths[0].lookAt( self.camera ) + + return task.cont + + def Transform( self ): + + startVec = self.startVec + + axis = self.GetSelectedAxis() + if axis is not None and axis.vector == CAMERA_VECTOR: + endVec = self.getRelativeVector( render, self.GetAxisPoint( axis ) - self.getPos() ) + + cross = startVec.cross( endVec ) + direction = self.getRelativeVector( self.camera, Vec3(0, -1, 0) ).dot( cross ) + sign = math.copysign( 1, direction ) + + # Get the rotation axis + rotAxis = self.getRelativeVector( self.camera, Vec3(0, -1, 0) ) * sign + else: + if self.collEntry.getIntoNode() == self.initCollEntry.getIntoNode(): + endVec = self.collEntry.getSurfaceNormal( self ) + else: + endVec = self.getRelativeVector( render, self.GetAxisPoint( self.foobar ) - self.getPos() ) + + # If an axis is selected then constrain the vectors by projecting + # them onto a plane whose normal is the axis vector + if axis is not None: + plane = Plane( axis.vector, Point3( 0 ) ) + startVec = Vec3( plane.project( Point3( startVec ) ) ) + endVec = Vec3( plane.project( Point3( endVec ) ) ) + + # Get the rotation axis + rotAxis = endVec.cross( startVec ) * -1 + + # Return if the rotation vector is not valid, ie it does not have any + # length + if not rotAxis.length(): + return + + # Normalize all vectors + startVec.normalize() + endVec.normalize() + rotAxis.normalize() + + # Get the amount of degrees to rotate + degs = startVec.angleDeg( endVec ) + + # Transform the gizmo if in local rotation mode + newRotMat = Mat4().rotateMat( degs, rotAxis ) + if self.local: + self.setMat( newRotMat * self.getMat() ) + + # Transform all attached node paths + for i, np in enumerate( self.attachedNps ): + + # Split the transform into scale, rotation and translation + # matrices + transMat, rotMat, scaleMat = commonUtils.GetTrsMatrices( np.getTransform() ) + + # Perform transforms in local or world space + if self.local: + np.setMat( scaleMat * newRotMat * rotMat * transMat ) + else: + self.initNpXforms[i].getQuat().extractToMatrix( rotMat ) + np.setMat( scaleMat * rotMat * newRotMat * transMat ) + + def OnNodeMouse1Down( self, planar, collEntry ): + Base.OnNodeMouse1Down( self, planar, collEntry ) + + # Store the initial collision entry + self.initCollEntry = collEntry + + # If the selected axis is the camera vector then use a point on the + # plane whose normal is the camera vector as the starting vector, + # otherwise use the surface normal from the collision with the sphere + axis = self.GetSelectedAxis() + if axis is not None and axis.vector == CAMERA_VECTOR: + self.startVec = self.getRelativeVector( render, self.initMousePoint - self.getPos() ) + else: + self.startVec = self.initCollEntry.getSurfaceNormal( self ) + + def OnMouse2Down( self ): + Base.OnMouse2Down( self ) + + axis = self.GetSelectedAxis() + if ( hasattr( self, 'collEntry' ) and hasattr( self, 'initCollEntry' ) and + self.collEntry.getIntoNode() != self.initCollEntry.getIntoNode() ): + self.startVec = self.getRelativeVector( render, self.GetAxisPoint( self.foobar ) - self.getPos() ) + else: + self.startVec = self.getRelativeVector( render, self.initMousePoint - self.getPos() ) + + def OnNodeMouseOver( self, collEntry ): + Base.OnNodeMouseOver( self, collEntry ) + + # Store the collision entry + self.collEntry = collEntry \ No newline at end of file diff --git a/src/pandaEditor/gizmos/scale.py b/src/pandaEditor/gizmos/scale.py new file mode 100644 index 0000000..739c75e --- /dev/null +++ b/src/pandaEditor/gizmos/scale.py @@ -0,0 +1,101 @@ +import math + +from pandac.PandaModules import Mat4, Vec3, Point3, CollisionSphere, NodePath + +from p3d import commonUtils +from p3d.geometry import Line, Box +from axis import Axis +from base import Base +from constants import * + + +class Scale( Base ): + + def __init__( self, *args, **kwargs ): + Base.__init__( self, *args, **kwargs ) + + self.complementary = False + + # Create x, y, z and center axes + self.axes.append( self.CreateBox( Vec3(1, 0, 0), RED ) ) + self.axes.append( self.CreateBox( Vec3(0, 1, 0), GREEN ) ) + self.axes.append( self.CreateBox( Vec3(0, 0, 1), BLUE ) ) + self.axes.append( self.CreateCenter( Vec3(1, 1, 1), TEAL ) ) + + def CreateBox( self, vector, colour ): + + # Create the geometry and collision + line = NodePath( Line( (0, 0, 0), vector ) ) + box = Box( 0.1, 0.1, 0.1, origin=Point3(0.05, 0.05, 0.05) + vector * 0.05 ) + collSphere = CollisionSphere( Point3( vector * -0.05 ), 0.1 ) + + # Create the axis, add the geometry and collision + axis = Axis( self.name, vector, colour ) + axis.AddGeometry( line, colour=GREY, highlight=False, sizeStyle=SCALE ) + axis.AddGeometry( box, vector, colour ) + axis.AddCollisionSolid( collSphere, vector ) + axis.reparentTo( self ) + + return axis + + def CreateCenter( self, vector, colour ): + + # Create the axis, add the geometry and collision + axis = Axis( self.name, vector, colour, default=True ) + axis.AddGeometry( Box( 0.1, 0.1, 0.1, origin=Point3(0.05, 0.05, 0.05) ), sizeStyle=NONE ) + axis.AddCollisionSolid( CollisionSphere( 0, 0.1 ), sizeStyle=NONE ) + axis.reparentTo( self ) + + return axis + + def Transform( self ): + + # Get the distance the mouse has moved during the drag operation - + # compensate for how big the gizmo is on the screen + axis = self.GetSelectedAxis() + axisPoint = self.GetAxisPoint( axis ) + distance = ( axisPoint - self.initMousePoint ).length() / self.getScale()[0] + + # Using length() will give us a positive number, which doesn't work if + # we're trying to scale down the object. Get the sign for the distance + # from the dot of the axis and the mouse direction + mousePoint = self.getRelativePoint( render, axisPoint ) - self.getRelativePoint( render, self.initMousePoint ) + direction = axis.vector.dot( mousePoint ) + sign = math.copysign( 1, direction ) + distance = distance * sign + + # Transform the gizmo + if axis.vector == Vec3(1, 1, 1): + for otherAxis in self.axes: + otherAxis.SetSize( distance + self.size ) + else: + axis.SetSize( distance + self.size ) + + # Use the "complementary" vector if in complementary mode + vector = axis.vector + if self.complementary: + vector = Vec3(1, 1, 1) - axis.vector + + # Create a scale matrix from the resulting vector + scaleVec = vector * ( distance + 1 ) + Vec3(1, 1, 1) - vector + newScaleMat = Mat4().scaleMat( scaleVec ) + + # Transform attached node paths + for i, np in enumerate( self.attachedNps ): + + # Perform transforms in local or world space + if self.local: + np.setMat( newScaleMat * self.initNpXforms[i].getMat() ) + else: + transMat, rotMat, scaleMat = commonUtils.GetTrsMatrices( self.initNpXforms[i] ) + np.setMat( scaleMat * rotMat * newScaleMat * transMat ) + + def OnNodeMouse1Down( self, planar, collEntry ): + + # Cheating a bit here. We just need the planar flag taken from the + # user ctrl-clicking the gizmo, none of the maths that come with it. + # We'll use the complementary during the transform operation. + self.complementary = planar + planar = False + + Base.OnNodeMouse1Down( self, planar, collEntry ) \ No newline at end of file diff --git a/src/pandaEditor/gizmos/translation.py b/src/pandaEditor/gizmos/translation.py new file mode 100644 index 0000000..b4c1559 --- /dev/null +++ b/src/pandaEditor/gizmos/translation.py @@ -0,0 +1,99 @@ +from pandac.PandaModules import Mat4, Vec3, Point3 +from pandac.PandaModules import CollisionTube, CollisionSphere, NodePath + +from p3d import commonUtils +from p3d.geometry import Cone, Square, Line +from axis import Axis +from base import Base +from constants import * + + +class Translation( Base ): + + def __init__( self, *args, **kwargs ): + Base.__init__( self, *args, **kwargs ) + + # Create x, y, z and camera normal axes + self.axes.append( self.CreateArrow( Vec3(1, 0, 0), RED ) ) + self.axes.append( self.CreateArrow( Vec3(0, 1, 0), GREEN ) ) + self.axes.append( self.CreateArrow( Vec3(0, 0, 1), BLUE ) ) + self.axes.append( self.CreateSquare( Vec3(0, 0, 0), TEAL ) ) + + def CreateArrow( self, vector, colour ): + + # Create the geometry and collision + line = NodePath( Line( (0, 0, 0), vector ) ) + cone = NodePath( Cone( 0.05, 0.25, axis=vector, origin=vector * 0.25 ) ) + collTube = CollisionTube( (0,0,0), Point3( vector ) * 0.95, 0.05 ) + + # Create the axis, add the geometry and collision + axis = Axis( self.name, vector, colour ) + axis.AddGeometry( line, sizeStyle=SCALE ) + axis.AddGeometry( cone, vector, colour ) + axis.AddCollisionSolid( collTube, sizeStyle=TRANSLATE_POINT_B ) + axis.reparentTo( self ) + + return axis + + def CreateSquare( self, vector, colour ): + + # Create the geometry and collision + self.square = NodePath( Square( 0.2, 0.2, Vec3(0, 1, 0), origin=Point3(0.1, 0.1, 0) ) ) + self.square.setBillboardPointEye() + collSphere = CollisionSphere( 0, 0.125 ) + + # Create the axis, add the geometry and collision + axis = Axis( self.name, CAMERA_VECTOR, colour, planar=True, default=True ) + axis.AddGeometry( self.square, sizeStyle=NONE ) + axis.AddCollisionSolid( collSphere, sizeStyle=NONE ) + axis.reparentTo( self ) + + return axis + + def Transform( self ): + + # Get the point where the mouse clicked the axis + axis = self.GetSelectedAxis() + axisPoint = self.GetAxisPoint( axis ) + + # Get the gizmo's translation matrix and transform it + newTransMat = Mat4().translateMat( self.initXform.getPos() - self.getPos() + axisPoint - self.initMousePoint ) + self.setMat( self.getMat() * newTransMat ) + + # Get the attached node path's translation matrix + transVec = axisPoint - self.initMousePoint + if axis.vector != CAMERA_VECTOR: + transVec = self.getRelativeVector( render, transVec ) * self.getScale()[0] + newTransMat = Mat4().translateMat( transVec ) + + # Transform attached node paths + for i, np in enumerate( self.attachedNps ): + + # Perform transforms in local or world space + if self.local and axis.vector != CAMERA_VECTOR: + transMat, rotMat, scaleMat = commonUtils.GetTrsMatrices( self.initNpXforms[i] ) + np.setMat( scaleMat * newTransMat * rotMat * transMat ) + else: + np.setMat( self.initNpXforms[i].getMat() * newTransMat ) + + def OnNodeMouse1Down( self, planar, collEntry ): + Base.OnNodeMouse1Down( self, planar, collEntry ) + + # Store the gizmo's initial transform + self.initXform = self.getTransform() + + # If in planar mode, clear the billboard effect on the center square + # and make it face the selected axis + axis = self.GetSelectedAxis() + if self.planar and not axis.planar: + self.square.clearBillboard() + self.square.lookAt( self, Point3( axis.vector ) ) + else: + self.square.setHpr( Vec3(0, 0, 0) ) + self.square.setBillboardPointEye() + + def OnMouse2Down( self ): + Base.OnMouse2Down( self ) + + # Store the gizmo's initial transform + self.initXform = self.getTransform() \ No newline at end of file diff --git a/src/pandaEditor/project.py b/src/pandaEditor/project.py new file mode 100644 index 0000000..525656d --- /dev/null +++ b/src/pandaEditor/project.py @@ -0,0 +1,279 @@ +import os +import sys +import shutil +import subprocess +import xml.etree.cElementTree as et + +import pandac.PandaModules as pm +from wx.lib.pubsub import Publisher as pub + +import utils + + +PROJECT_DEF_NAME = 'project.xml' +SHADER_FILE_NAME = 'shader.sha' + +MAPS = 'maps' +MODELS = 'models' +SCRIPTS = 'scripts' +PREFABS = 'prefabs' +SHADERS = 'shaders' + + +class DirectoryWatcher( utils.DirectoryWatcher ): + + def onAdded( self, filePaths ): + messenger.send( 'projectFilesAdded', [filePaths] ) + pub.sendMessage( 'projectFilesAdded', filePaths ) + + def onRemoved( self, filePaths ): + messenger.send( 'projectFilesRemoved', [filePaths] ) + pub.sendMessage( 'projectFilesRemoved', filePaths ) + + def onModified( self, filePaths ): + messenger.send( 'projectFilesModified', [filePaths] ) + pub.sendMessage( 'projectFilesModified', filePaths ) + + +class Project( object ): + + def __init__( self, app ): + self.app = app + + self.path = None + self.dirs = {} + + # Create directory watcher + self.dirWatcher = DirectoryWatcher() + + def GetProjectDefinitionPath( self ): + return os.path.join( self.path, PROJECT_DEF_NAME ) + + def GetMainScript( self ): + return """from direct.directbase import DirectStart + +import game + + +# Create game base and load level +game = game.Base() +game.OnInit() +game.scnParser.Load( render, 'maps/test.xml' ) +run()""" + + def New( self, dirPath, **kwargs ): + + dirs = { + MAPS:kwargs.pop( 'maps', 'maps' ), + MODELS:kwargs.pop( 'models', 'models' ), + SCRIPTS:kwargs.pop( 'scripts', 'scripts' ), + PREFABS:kwargs.pop( 'prefabs', 'prefabs' ), + SHADERS:kwargs.pop( 'shaders', 'shaders' ) + } + + # Create xml tags for project directories + rootElem = et.Element( 'Project' ) + for dirType, dirName in dirs.items(): + elem = et.SubElement( rootElem, 'Directory' ) + elem.set( 'type', dirType ) + elem.set( 'name', dirName ) + + # Wrap with an element tree and write to file + tree = et.ElementTree( rootElem ) + utils.Indent( tree.getroot() ) + filePath = os.path.join( dirPath, PROJECT_DEF_NAME ) + tree.write( filePath ) + + self.path = dirPath + + # Create directories + self.CreateDirectories( dirs.values() ) + + # Create a main.py stub + self.CreateAsset( os.path.join( dirPath, 'main.py' ), self.GetMainScript() ) + + def Set( self, dirPath ): + + # Check project definition file exists + if not os.path.exists( os.path.join( dirPath, PROJECT_DEF_NAME ) ): + self.path = None + return + + # Set the project directory + oldPath = self.path + self.path = dirPath + + # Clear the model search path and add the new project path. Make sure + # to prepend the new directory or else Panda might search in-built + # paths first and supply the incorrect model. + base.ResetModelPath() + modelPath = pm.Filename.fromOsSpecific( self.path ) + pm.getModelPath().prependDirectory( modelPath ) + + # Remove the old project path from sys.path and add the new one + if oldPath in sys.path: + sys.path.remove( oldPath ) + sys.path.insert( 0, self.path ) + + # Set paths + self.SetDirectories() + + # Set directory watcher root path and start it + self.dirWatcher.setDirectory( self.path ) + if not self.dirWatcher.running: + self.dirWatcher.start() + + def CreateDirectories( self, dirNames ): + + # Create project directories + for dirName in dirNames: + os.makedirs( os.path.join( self.path, dirName ) ) + + def SetDirectories( self ): + """ + + """ + self.dirs = {} + + projPath = self.GetProjectDefinitionPath() + tree = et.parse( projPath ) + rootElem = tree.getroot() + dirsElem = rootElem.findall( 'Directory' ) + for dirElem in dirsElem: + dirName = dirElem.get( 'name' ) + dirType = dirElem.get( 'type' ) + self.dirs[dirType] = dirName + + def GetDirectory( self, dirType ): + """Return the full path to the indicated directory type.""" + if self.path is not None and self.dirs: + return os.path.join( self.path, self.dirs[dirType] ) + else: + return None + + def GetMapsDirectory( self ): + """Return the full path to the maps directory.""" + return self.GetDirectory( MAPS ) + + def GetModelsDirectory( self ): + """Return the full path to the models directory.""" + return self.GetDirectory( MODELS ) + + def GetScriptsDirectory( self ): + """Return the full path to the scripts directory.""" + return self.GetDirectory( SCRIPTS ) + + def GetPrefabsDirectory( self ): + """Return the full path to the prefabs directory.""" + return self.GetDirectory( PREFABS ) + + def GetShadersDirectory( self ): + """Return the full path to the shaders directory.""" + return self.GetDirectory( SHADERS ) + + def ImportAsset( self, filePath ): + + # Strip all extensions + bamName = os.path.basename( filePath ) + while True: + bamName, ext = os.path.splitext( bamName ) + if not ext: + break + + # Call egg to bam and create a bam in the model directory + modelsPath = self.GetModelsDirectory() + tgtPath = os.path.join( modelsPath, bamName ) + '.bam' + subprocess.call(['egg2bam', '-o', tgtPath, filePath]) + + def GetUniqueAssetName( self, startName, dirPath ): + """Return a unique asset name.""" + newAssetName = startName + assetNames = os.listdir( dirPath ) + + # Iterate until we find a incremented suffix not in use + i = 1 + while True: + if newAssetName not in assetNames: + break + baseName, ext = os.path.splitext( startName ) + newAssetName = ''.join( [baseName, str( i ), ext] ) + i += 1 + + # Return the full path to the new asset + return os.path.join( dirPath, newAssetName ) + + def CreateAsset( self, filePath, contents ): + """Base method for creating a file asset in a project folder.""" + file = open( filePath, 'w' ) + file.write( contents ) + file.close() + + def CreateCgShader( self ): + """Create a new cg shader in the shaders directory.""" + dirPath = self.GetShadersDirectory() + shaderPath = self.GetUniqueAssetName( SHADER_FILE_NAME, dirPath ) + shader = '' + self.CreateAsset( shaderPath, shader ) + + def CreateBuildScript( self, fileName ): + return """import sys +sys.path.append( 'F:/Python' ) +sys.path.append( 'F:/Python/pandaEditor 0.1/pandaEditor' ) + + +class """ + fileName + """( p3d ): + require( 'morepy' ) + require( 'audio' ) + mainModule( 'main' ) + dir( 'maps', newDir='maps' ) + dir( 'models', newDir='models' ) + dir( 'sounds', newDir='sounds' ) + dir( 'scripts', newDir='scripts' ) + dir( 'userPlugins', newDir='userPlugins' )""" + + def Build( self, buildPath ): + """Build the project to a p3d file.""" + # Check if we can create a directory here with the same name as the + # project + tempDirPath = os.path.splitext( buildPath )[0] + if os.path.exists( tempDirPath ): + print 'Already a directory named ', tempDirPath + return False + + # Copy the entire project the temp location + shutil.copytree( self.path, tempDirPath ) + + # Now copy the plugin module + pluginsPath = self.app.game.pluginMgr.GetPluginsPath() + if pluginsPath is not None: + pluginDestPath = os.path.join( tempDirPath, 'userPlugins' ) + shutil.copytree( self.app.game.pluginMgr.GetPluginsPath(), + pluginDestPath ) + + # Remove all editor plugins for the userPlugins directory + for dirName in os.listdir( pluginDestPath ): + dirPath = os.path.join( pluginDestPath, dirName ) + if os.path.isdir( dirPath ) and 'editorPlugin' in os.listdir( dirPath): + shutil.rmtree( os.path.join( dirPath, 'editorPlugin' ) ) + + # Parse build script + buildDirPath, buildName = os.path.split( buildPath ) + scriptLines = self.CreateBuildScript( os.path.splitext( buildName )[0] ) + buildDefPath = os.path.join( tempDirPath, 'build.pdef' ) + file = open( buildDefPath, 'w' ) + file.writelines( scriptLines ) + file.close() + + # DEBUG + # Turn scripts into a module otherwise ppackage won't find it. + scriptsPath = os.path.join( tempDirPath, 'scripts', '__init__.py' ) + file = open( scriptsPath, 'w' ) + file.close() + + # Run the script with ppackage + subprocess.Popen( ['ppackage', '-i', buildDirPath, 'build.pdef'], + #stdout=sys.stdout, stderr=sys.stderr, + cwd=tempDirPath ) + + # Clean up + #shutil.rmtree( tempDirPath ) \ No newline at end of file diff --git a/src/pandaEditor/scene.py b/src/pandaEditor/scene.py new file mode 100644 index 0000000..8fd7bc6 --- /dev/null +++ b/src/pandaEditor/scene.py @@ -0,0 +1,124 @@ +import copy + +import pandac.PandaModules as pm + +import p3d +import game +import editor + + +class Scene( p3d.Object ): + + def __init__( self, app, *args, **kwargs ): + self.app = app + + self.filePath = kwargs.pop( 'filePath', None ) + p3d.Object.__init__( self, *args, **kwargs ) + + # Tag default nodes + base.cam.setTag( game.nodes.TAG_NODE_TYPE, 'BaseCam' ) + base.camera.setTag( game.nodes.TAG_NODE_TYPE, 'BaseCamera' ) + + # Call create to run editor create methods. + base.game.nodeMgr.Create( 'BaseCam' ) + base.game.nodeMgr.Create( 'BaseCamera' ) + + def Load( self, **kwargs ): + """Recreate a scene graph from file.""" + filePath = kwargs.get( 'filePath', self.filePath ) + if filePath is None: + return False + + self.app.game.scnParser.Load( self.rootNp, filePath ) + + def Save( self, **kwargs ): + """Save a scene graph to file.""" + filePath = kwargs.get( 'filePath', self.filePath ) + if filePath is None: + return False + + self.app.game.scnParser.Save( self.rootNp, filePath ) + + def Close( self ): + """Destroy the scene by removing all its nodes.""" + def Destroy( np, cookie ): + wrpr = base.game.nodeMgr.Wrap( np ) + if wrpr is not None: + wrpr.Destroy() + + self.Walk( Destroy ) + self.app.game.pluginMgr.OnSceneClose() + + # Now remove the root node. If the root node was render, reset base + # in order to remove and recreate the default node set. + if self.rootNp is render: + base.Reset() + + # Reset references to render + self.app.selection.marquee.rootNp = render + + self.rootNp.removeNode() + + def AddNodePaths( self, nps ): + """Parent the indicated node paths to the scene root.""" + for np in nps: + np.reparentTo( self.rootNp ) + + def DeleteNodePaths( self, nps ): + """ + Delete the indicated node paths, taking care to destroy the node's + helper if there is one. + """ + for np in nps: + np.detachNode() + + def DuplicateNodePaths( self, nps ): + """Duplicate node paths.""" + dupeNps = [] + + for np in nps: + + # Copy the node, parenting to the same parent as the original + dupeNp = np.copyTo( np.getParent() ) + dupeNps.append( dupeNp ) + + # Call duplicate methods for any wrappers + wrpr = base.game.nodeMgr.Wrap( np ) + if wrpr is not None: + wrpr.Duplicate( np, dupeNp ) + + return dupeNps + + def Walk( self, func, np=None, arg=None, includeHelpers=False, modelRootsOnly=True ): + """Walk hierarchy calling func on each member.""" + if np is None: + np = self.rootNp + + # Bail if helpers were not included and this node represents a helper + # geometry or collision + if not includeHelpers and np.getPythonTag( editor.nodes.TAG_IGNORE ): + return + + children = np.getChildren() + func( np, arg ) + + # Flag as being under a model root before processing children. + # WARNING: this may play badly with eggs that have nested children. + isActor = False + pObj = p3d.PandaObject.Get( np ) + if pObj is not None and hasattr( pObj, 'actor' ) and pObj.actor: + isActor = True + self.underModelRoot = True + + isModelRoot = np.node().isOfType( pm.ModelRoot ) + if isModelRoot: + self.underModelRoot = True + + if not modelRootsOnly or ( modelRootsOnly and not + np.node().isOfType( pm.ModelRoot ) ): + for child in children: + self.Walk( func, child, arg, includeHelpers, modelRootsOnly ) + + # Finished processing children - reset underModelRoot flag. + if isModelRoot or isActor: + self.underModelRoot = False \ No newline at end of file diff --git a/src/pandaEditor/selection.py b/src/pandaEditor/selection.py new file mode 100644 index 0000000..c9be1be --- /dev/null +++ b/src/pandaEditor/selection.py @@ -0,0 +1,190 @@ +import wx +from direct.directtools.DirectSelection import DirectBoundingBox +import pandac.PandaModules as pm + +import p3d +import editor +from scene import Scene + + +class Selection( p3d.Object ): + + BBOX_TAG = 'bbox' + + def __init__( self, *args, **kwargs ): + p3d.Object.__init__( self, *args, **kwargs ) + + self.nps = [] + + # Create a marquee + self.marquee = p3d.Marquee( 'marquee', *args, **kwargs ) + + # Create node picker - set its collision mask to hit both geom nodes + # and collision nodes + bitMask = pm.GeomNode.getDefaultCollideMask() | pm.CollisionNode.getDefaultCollideMask() + self.picker = p3d.MousePicker( 'picker', *args, fromCollideMask=bitMask, **kwargs ) + self.picker.Start() + + def AddBBox( self, np ): + """Add a bounding box to the indicated node.""" + bbox = DirectBoundingBox( np, (1, 0, 0, 1) ) + bbox.show() + bbox.lines.setPythonTag( editor.nodes.TAG_IGNORE, True ) + bbox.lines.node().adjustDrawMask( *base.GetEditorRenderMasks() ) + np.setPythonTag( self.BBOX_TAG, bbox ) + return bbox + + def RemoveBBox( self, np ): + """Remove the bounding box from the indicated node.""" + bbox = np.getPythonTag( self.BBOX_TAG ) + if bbox is not None: + bbox.lines.removeNode() + np.clearPythonTag( self.BBOX_TAG ) + + def Get( self ): + """Return the selected node paths.""" + return self.nps + + def Clear( self ): + """Clear the selection list and remove all bounding boxes.""" + for np in self.nps: + if not np.isEmpty(): + self.RemoveBBox( np ) + self.nps = [] + + def Add( self, nps ): + """Add the indicated node paths to the selection.""" + for np in list( set( nps ) ): + if not np.isEmpty(): + self.AddBBox( np ) + self.nps.append( np ) + + # Run OnSelect handlers + wrpr = base.game.nodeMgr.Wrap( np ) + if wrpr is not None: + wrpr.OnSelect( np ) + + def Remove( self, nps ): + """Remove those node paths that were in the selection.""" + for np in nps: + if not np.isEmpty(): + self.RemoveBBox( np ) + + # Run OnDeselect handlers + wrpr = base.game.nodeMgr.Wrap( np ) + if wrpr is not None: + wrpr.OnDeselect( np ) + + self.nps = list( set( self.nps ) - set( nps ) ) + + def SelectParent( self ): + """Select parent node paths.""" + nps = [] + for np in self.nps: + if np.getParent() != render: + nps.append( np.getParent() ) + else: + nps.append( np ) + return nps + + def SelectChild( self ): + """Select child node paths.""" + nps = [] + for np in self.nps: + children = np.getChildren() + isIgnore = [child.getPythonTag( editor.nodes.TAG_IGNORE ) for child in children] + if children and not isIgnore[0]: + nps.append( children[0] ) + else: + nps.append( np ) + return nps + + def SelectPrev( self ): + """Select previous node paths.""" + nps = [] + for np in self.nps: + + # Find where the child appears in the list + children = list( np.getParent().getChildren() ) + index = children.index( np ) - 1 + + # Wrap around if the index has gone below zero + if index < 0: + index = len( children ) - 1 + + nps.append( children[index] ) + return nps + + def SelectNext( self ): + """Select next node paths.""" + nps = [] + for np in self.nps: + + # Find where the child appears in the list + children = list( np.getParent().getChildren() ) + index = children.index( np ) + 1 + + # Wrap around if the index has gone below zero + if index > len( children ) - 1: + index = 0 + + nps.append( children[index] ) + return nps + + def StartDragSelect( self, append=False ): + """ + Start the marquee and put the tool into append mode if specified. + """ + if self.marquee.mouseWatcherNode.hasMouse(): + self.append = append + self.marquee.Start() + + def StopDragSelect( self ): + """ + Stop the marquee and get all the node paths under it with the correct + tag. Also append any node which was under the mouse at the end of the + operation. + """ + nps = [] + + # Stop the marquee + self.marquee.Stop() + + # Find all node paths below the root node which are inside the marquee + # AND have the correct tag + for np in self.rootNp.findAllMatches( '**' ): + if ( self.marquee.IsNodePathInside( np ) and + np.getPythonTag( editor.nodes.TAG_PICKABLE ) ): + nps.append( np ) + + # Add any node path which was under the mouse to the selection + np = self.GetNodePathUnderMouse() + if np is not None: + nps.append( np ) + + # In append mode we want to add / remove items from the current + # selection + if self.append: + for selfNp in self.nps: + if selfNp in nps: + nps.remove( selfNp ) + else: + nps.append( selfNp ) + + # Clear current selection and select new node paths + return nps + + def GetNodePathUnderMouse( self ): + """ + Returns the closest node under the mouse, or None if there isn't one. + """ + pickedNp = self.picker.GetFirstNodePath() + if pickedNp is not None: + return pickedNp.findNetPythonTag( editor.nodes.TAG_PICKABLE ) + else: + return None + + def Update( self ): + for np in self.nps: + self.RemoveBBox( np ) + self.AddBBox( np ) \ No newline at end of file diff --git a/src/pandaEditor/showBase.py b/src/pandaEditor/showBase.py new file mode 100644 index 0000000..c9f7eb5 --- /dev/null +++ b/src/pandaEditor/showBase.py @@ -0,0 +1,174 @@ +import pandac.PandaModules as pm +from direct.showbase import ShowBase as P3dShowBase + +import p3d + + +class ShowBase( P3dShowBase.ShowBase ): + + def __init__( self, wxGameWin, wxEdWin, *args, **kwargs ): + P3dShowBase.ShowBase.__init__( self, *args, **kwargs ) + + self.SetupEdRender() + self.SetupEdRender2d() + self.SetupEdWindow( wxGameWin, wxEdWin ) + self.SetupEdMouseWatcher() + self.SetupEdCamera() + + # Make additional camera for 2d nodes + cam2d = self.makeCamera2d( self.edWin ) + cam2d.reparentTo( self.edRender2d ) + + # Add the editor window, camera and pixel 2d to the list of forced + # aspect windows so aspect is fixed when the window is resized. + self.forcedAspectWins = [] + self.forcedAspectWins.append( (self.edWin, self.edCamera, self.edPixel2d) ) + + # Set up masks for camera and render + self.SetupCameraMask() + self.SetupRenderMask() + + def SetupEdRender( self ): + """ + Create editor root node behind render node so we can keep editor only + nodes out of the scene. + """ + self.edRender = pm.NodePath( 'edRender' ) + self.edRender.setShaderAuto() + render.reparentTo( self.edRender ) + + def SetupEdRender2d( self ): + """ + Creates the render2d scene graph, the primary scene graph for 2-d + objects and gui elements that are superimposed over the 3-d geometry + in the window. + """ + self.edRender2d = pm.NodePath( 'edRender2d' ) + + # Set up some overrides to turn off certain properties which we + # probably won't need for 2-d objects. + self.edRender2d.setDepthTest( 0 ) + self.edRender2d.setDepthWrite( 0 ) + self.edRender2d.setMaterialOff( 1 ) + self.edRender2d.setTwoSided( 1 ) + + # This special root, pixel2d, uses units in pixels that are relative + # to the window. The upperleft corner of the window is (0, 0), + # the lowerleft corner is (xsize, -ysize), in this coordinate system. + xsize, ysize = self.getSize() + self.edPixel2d = self.edRender2d.attachNewNode( pm.PGTop( 'edPixel2d' ) ) + self.edPixel2d.setPos( -1, 0, 1 ) + if xsize > 0 and ysize > 0: + self.edPixel2d.setScale( 2.0 / xsize, 1.0, 2.0 / ysize ) + + def SetupEdWindow( self, wxGameWin, wxEdWin ): + wxGameWin.Initialize() + wxEdWin.Initialize( useMainWin=False ) + self.edWin = wxEdWin.GetWindow() + + def SetupEdMouseWatcher( self ): + + # Setup mouse watcher for the editor window + buttonThrowers, pointerWatcherNodes = self.setupMouseCB( self.edWin ) + self.edMouseWatcher = buttonThrowers[0].getParent() + self.edMouseWatcherNode = self.edMouseWatcher.node() + + def SetupEdCamera( self ): + + # Create editor camera + self.edCamera = p3d.EditorCamera( + 'camera', + style=p3d.CAM_VIEWPORT_AXES, + speed=0.5, + rootP2d=self.edPixel2d, + win=self.edWin, + mouseWatcherNode=self.edMouseWatcherNode + ) + self.edCamera.reparentTo( self.edRender ) + self.edCamera.Start() + + # Fix camera pathing + dr = self.edWin.getDisplayRegion( 0 ) + dr.setActive( True ) + dr.setCamera( self.edCamera ) + + def windowEvent( self, *args, **kwargs ): + """ + Overridden so as to fix the aspect ratio of the editor camera and + editor pixel2d. + """ + P3dShowBase.ShowBase.windowEvent( self, *args, **kwargs ) + + for win, cam, pixel2d in self.forcedAspectWins: + aspectRatio = self.getAspectRatio( win ) + cam.node().getLens().setAspectRatio( aspectRatio ) + + # Fix pixel2d scale for new window size + # Temporary hasattr for old Pandas + if not hasattr( win, 'getSbsLeftXSize' ): + pixel2d.setScale( 2.0 / win.getXSize(), 1.0, 2.0 / win.getYSize() ) + else: + pixel2d.setScale( 2.0 / win.getSbsLeftXSize(), 1.0, 2.0 / win.getSbsLeftYSize() ) + + def GetEditorRenderMasks( self ): + """ + Return the show, hide and clear masks for objects that are to be + rendered only in the editor viewport. + """ + show = pm.BitMask32() + show.setRangeTo( True, 28, 4 ) + hide = pm.BitMask32().allOn() + hide.setRangeTo( False, 28, 4 ) + clear = pm.BitMask32() + + return show, hide, clear + + def SetupCameraMask( self ): + """ + Set camera mask to draw all objects but those with the first four bits + flipped. All editor geometry will use these bits so as to not be + rendered in the game view. + """ + bits = self.cam.node().getCameraMask() + bits.setRangeTo( False, 28, 4 ) + self.cam.node().setCameraMask( bits ) + + # Set edRender mask + self.edRender.node().adjustDrawMask( *self.GetEditorRenderMasks() ) + + def SetupRenderMask( self ): + """ + Set the draw mask for the render node to be visible to all cameras. + Since we are adjusting the draw mask of the render node's parent we + need to manually set this node's mask or it will inherit those + properties. + """ + showMask = pm.BitMask32().allOn() + hideMask = pm.BitMask32() + clearMask = pm.BitMask32() + render.node().adjustDrawMask( showMask, hideMask, clearMask ) + + def Reset( self ): + """Remove all default nodes and recreate them.""" + # Remove cam node and camera + self.cam.removeNode() + self.cam = None + self.camera.removeNode() + self.camera = None + + # Recreate all nodes + self.setupRender() + self.makeCamera( self.win ) + __builtins__['render'] = self.render + + # Set up masks + self.SetupCameraMask() + self.SetupRenderMask() + + def ResetModelPath( self ): + """ + Clears the model path, making sure to restore the current working + directory (so editor models can still be found). + """ + pm.getModelPath().clear() + pm.getModelPath().prependDirectory( '.' ) \ No newline at end of file diff --git a/src/pandaEditor/ui/__init__.py b/src/pandaEditor/ui/__init__.py new file mode 100644 index 0000000..e2a12c9 --- /dev/null +++ b/src/pandaEditor/ui/__init__.py @@ -0,0 +1,11 @@ +from document import Document +from baseDialog import BaseDialog +from mainFrame import MainFrame +from viewport import Viewport +from projectSettingsPanel import ProjectSettingsPanel +from consolePanel import ConsolePanel +from propertiesPanel import PropertiesPanel +from resourcesPanel import ResourcesPanel +from sceneGraphBasePanel import SceneGraphBasePanel +from sceneGraphPanel import SceneGraphPanel +import customProperties \ No newline at end of file diff --git a/src/pandaEditor/ui/baseDialog.py b/src/pandaEditor/ui/baseDialog.py new file mode 100644 index 0000000..f757c08 --- /dev/null +++ b/src/pandaEditor/ui/baseDialog.py @@ -0,0 +1,22 @@ +import wx + + +class BaseDialog( wx.Dialog ): + + def __init__( self, Panel, *args, **kwargs ): + wx.Dialog.__init__( self, *args, **kwargs ) + + # Build the panel + self.pnl = Panel( self ) + + # Build sizers + bs1 = wx.BoxSizer( wx.VERTICAL ) + bs1.Add( self.pnl, 1, wx.EXPAND, 0 ) + self.SetSizer( bs1 ) + + # Resize list control to fit + #self.SetSize( (self.GetSize().x, self.pnl.GetBestSizeY()) ) + self.Fit() + + # Layout + self.CenterOnScreen() \ No newline at end of file diff --git a/src/pandaEditor/ui/consolePanel.py b/src/pandaEditor/ui/consolePanel.py new file mode 100644 index 0000000..68d0631 --- /dev/null +++ b/src/pandaEditor/ui/consolePanel.py @@ -0,0 +1,7 @@ +import wx + + +class ConsolePanel( wx.Panel ): + + def __init__( self, *args, **kwargs ): + wx.Panel.__init__( self, *args, **kwargs ) diff --git a/src/pandaEditor/ui/customProperties.py b/src/pandaEditor/ui/customProperties.py new file mode 100644 index 0000000..b387625 --- /dev/null +++ b/src/pandaEditor/ui/customProperties.py @@ -0,0 +1,123 @@ +import wx +import wx.lib.agw.floatspin as fs +import pandac.PandaModules as pm + +from wxExtra import wxpg, CompositeDropTarget + + +class Float3Property( wxpg.BaseProperty ): + + def __init__( self, *args, **kwargs ): + wxpg.BaseProperty.__init__( self, *args, **kwargs ) + + self._ctrls = [] + self._count = 3 + self._cast = pm.Vec3 # HAXXOR + + def BuildControl( self, parent, id ): + ctrl = wx.BoxSizer( wx.HORIZONTAL ) + for i in range( self._count ): + spin = fs.FloatSpin( parent, id, value=self._value[i] ) + spin.Bind( fs.EVT_FLOATSPIN, self.OnChanged ) + ctrl.Add( spin, 1, wx.EXPAND ) + self._ctrls.append( spin ) + + return ctrl + + def SetValueFromEvent( self, evt ): + ctrl = evt.GetEventObject() + index = self._ctrls.index( ctrl ) + self._value = self._cast( self._value ) + self._value[index] = ctrl.GetValue() + + +class Float2Property( Float3Property ): + + def __init__( self, *args, **kwargs ): + Float3Property.__init__( self, *args, **kwargs ) + + self._count = 2 + self._cast = pm.Vec2 + + +class Float4Property( Float3Property ): + + def __init__( self, *args, **kwargs ): + Float3Property.__init__( self, *args, **kwargs ) + + self._count = 4 + self._cast = pm.Vec4 + + +class Point2Property( Float3Property ): + + def __init__( self, *args, **kwargs ): + Float3Property.__init__( self, *args, **kwargs ) + + self._count = 2 + self._cast = pm.Point2 + + +class Point3Property( Float3Property ): + + def __init__( self, *args, **kwargs ): + Float3Property.__init__( self, *args, **kwargs ) + + self._count = 3 + self._cast = pm.Point3 + + +class Point4Property( Float3Property ): + + def __init__( self, *args, **kwargs ): + Float3Property.__init__( self, *args, **kwargs ) + + self._count = 4 + self._cast = pm.Point4 + + +""" +class AnimationDictProperty( wxpg.PyStringProperty ): + + def __init__( self, *args, **kwargs ): + self.animDict = kwargs.pop( 'attr' ) + wxpg.PyStringProperty.__init__( self, *args, **kwargs ) + + for animName, animPath in self.animDict.items(): + self.AddPrivateChild( wxpg.FileProperty( animName, wxpg.LABEL_AS_NAME, str( animPath ) ) ) + + def GetValueAsString( self, argFlags ): + return ', '.join( self.animDict.keys() ) +""" + + +class NodePathProperty( wxpg.StringProperty ): + + def BuildControl( self, parent, id ): + np = self.GetValue() + text = '' + if np is not None: + text = np.getName() + + ctrl = wx.TextCtrl( parent, id, value=text ) + ctrl.Bind( wx.EVT_TEXT, self.OnChanged ) + + dt = CompositeDropTarget( ['nodePath', 'filePath'], + self.OnDropItem, + self.ValidateDropItem ) + ctrl.SetDropTarget( dt ) + + return ctrl + + def ValidateDropItem( self, x, y ): + return True + + def OnDropItem( self, arg ): + np = wx.GetApp().frame.pnlSceneGraph.dragNps[0] + self.SetValue( np ) + + # Call after otherwise we crash! + evt = wxpg.PropertyGridEvent( wxpg.wxEVT_PG_CHANGED ) + evt.SetProperty( self ) + fn = lambda event: self.GetGrid().GetEventHandler().ProcessEvent( evt ) + wx.CallAfter( fn ) \ No newline at end of file diff --git a/src/pandaEditor/ui/document.py b/src/pandaEditor/ui/document.py new file mode 100644 index 0000000..cfd8154 --- /dev/null +++ b/src/pandaEditor/ui/document.py @@ -0,0 +1,59 @@ +import os + +import wx +from wx.lib.pubsub import Publisher as pub + +import pandaEditor + + +class Document( object ): + + def __init__( self, contents ): + self.contents = contents + + self.dirty = False + self.title = self.GetTitle() + + def GetTitle( self ): + if self.contents.filePath is not None: + return os.path.basename( self.contents.filePath ) + else: + return 'untitled' + + def Load( self ): + self.contents.Load() + self.OnRefresh() + + def Save( self, *args ): + self.contents.Save( *args ) + self.title = self.GetTitle() + self.dirty = False + self.OnRefresh() + + def OnSelectionChanged( self ): + """ + Broadcast the update selection message. Methods subscribed to this + message should be quick and not force full rebuilds of ui widgets + considering how quickly the selection is likely to change. + """ + pub.sendMessage( 'UpdateSelection', wx.GetApp().selection.nps ) + + def OnSelectionModified( self, task ): + + pub.sendMessage( 'SelectionModified', wx.GetApp().selection.nps ) + return task.cont + + def OnRefresh( self ): + """ + Broadcast the update message without setting the dirty flag. Methods + subscribed to this message will rebuild ui widgets completely. + """ + pub.sendMessage( 'Update', self ) + + def OnModified( self ): + """ + Broadcast the update message and set the dirty flag. Methods + subscribed to this message will rebuild ui widgets completely. + """ + self.dirty = True + pub.sendMessage( 'Update', self ) diff --git a/src/pandaEditor/ui/lightLinkerPanel.py b/src/pandaEditor/ui/lightLinkerPanel.py new file mode 100644 index 0000000..baf1c8d --- /dev/null +++ b/src/pandaEditor/ui/lightLinkerPanel.py @@ -0,0 +1,23 @@ +import wx +import wx.lib.agw.customtreectrl as ct + +from sceneGraphBasePanel import SceneGraphBasePanel + + +class LightLinkerPanel( wx.Panel ): + + def __init__( self, *args, **kwargs ): + wx.Panel.__init__( self, *args, **kwargs ) + + # Build splitter and panels + self.splt = wx.SplitterWindow( self ) + pnlLeft = SceneGraphBasePanel( self.splt ) + pnlRight = SceneGraphBasePanel( self.splt ) + + # Split the window + self.splt.SplitVertically( pnlLeft, pnlRight ) + self.splt.SetMinimumPaneSize( 20 ) + + sizer = wx.BoxSizer( wx.VERTICAL ) + sizer.Add( self.splt, 1, wx.EXPAND ) + self.SetSizer( sizer ) \ No newline at end of file diff --git a/src/pandaEditor/ui/mainFrame.py b/src/pandaEditor/ui/mainFrame.py new file mode 100644 index 0000000..1d4e11b --- /dev/null +++ b/src/pandaEditor/ui/mainFrame.py @@ -0,0 +1,616 @@ +import os +import sys + +import wx +import wx.aui +import wx.lib.agw.aui +from wx.lib.pubsub import Publisher as pub +import pandac.PandaModules as pm + +import p3d +from .. import commands as cmds +from wxExtra import utils as wxUtils, ActionItem, LogPanel, CustomMenu +from wxExtra import AuiManagerConfig, CustomAuiToolBar, CustomMenu +from viewport import Viewport +from pandaEditor import Scene, game +from document import Document +from baseDialog import BaseDialog +from resourcesPanel import ResourcesPanel +from propertiesPanel import PropertiesPanel +from sceneGraphPanel import SceneGraphPanel +from projectSettingsPanel import ProjectSettingsPanel + + +FRAME_TITLE = 'Panda Editor 0.1' +TBAR_ICON_SIZE = (24, 24) +WILDCARD_MODEL = 'Model (*.egg; *.bam; *.egg.pz)|*.egg;*.bam;*.egg.pz' +WILDCARD_SCENE = '.xml|*.xml' +WILDCARD_P3D = '.p3d|*.p3d' + +ID_FILE_NEW = wx.NewId() +ID_FILE_OPEN = wx.NewId() +ID_FILE_SAVE = wx.NewId() +ID_FILE_SAVE_AS = wx.NewId() +ID_FILE_IMPORT = wx.NewId() +ID_FILE_PROJ = wx.NewId() + +ID_PROJ_NEW = wx.NewId() +ID_PROJ_SET = wx.NewId() +ID_PROJ_BUILD = wx.NewId() + +ID_VIEW_GRID = wx.NewId() + +ID_WIND_FILE_TOOLBAR = wx.NewId() +ID_WIND_EDIT_TOOLBAR = wx.NewId() +ID_WIND_VIEWPORT = wx.NewId() +ID_WIND_SCENE_GRAPH = wx.NewId() +ID_WIND_PROPERTIES = wx.NewId() +ID_WIND_RESOURCES = wx.NewId() +ID_WIND_LOG = wx.NewId() + +ID_PLAY = wx.NewId() +ID_PAUSE = wx.NewId() + + +class MainFrame( wx.Frame ): + + """Panda Editor user interface.""" + + def __init__( self, *args, **kwargs ): + wx.Frame.__init__( self, *args, **kwargs ) + + self.app = wx.GetApp() + self.preMaxPos = None + self.preMaxSize = None + + # Bind frame events + self.Bind( wx.EVT_CLOSE, self.OnClose ) + self.Bind( wx.EVT_KEY_UP, p3d.wx.OnKeyUp ) + self.Bind( wx.EVT_KEY_DOWN, p3d.wx.OnKeyDown ) + self.Bind( wx.EVT_SIZE, self.OnSize ) + self.Bind( wx.EVT_MOVE, self.OnMove ) + + # Bind publisher events + pub.subscribe( self.OnUpdate, 'Update' ) + + # Build application preferences + self.cfg = wx.Config( 'pandaEditor' ) + + # Build toolbars + self.BuildFileActions() + self.BuildEditActions() + + # Build game and editor viewports. Don't initialise just yet as + # ShowBase has not yet been created. + self.pnlGameView = Viewport( self ) + self.pnlEdView = Viewport( self ) + + # Build viewport notebook + nbStyle = ( wx.aui.AUI_NB_DEFAULT_STYLE &~ + wx.aui.AUI_NB_CLOSE_ON_ACTIVE_TAB ) + self.nbViewport = wx.aui.AuiNotebook( self, style=nbStyle ) + self.nbViewport.AddPage( self.pnlEdView, 'Editor' ) + self.nbViewport.AddPage( self.pnlGameView, 'Game' ) + self.nbViewport.Bind( wx.EVT_KEY_UP, p3d.wx.OnKeyUp ) + self.nbViewport.Bind( wx.EVT_KEY_DOWN, p3d.wx.OnKeyDown ) + self.nbViewport.Bind( wx.EVT_LEFT_UP, p3d.wx.OnLeftUp ) + + # Build editor panels + self.pnlSceneGraph = SceneGraphPanel( self, style=wx.SUNKEN_BORDER ) + self.pnlProps = PropertiesPanel( self, style=wx.SUNKEN_BORDER ) + self.pnlRsrcs = ResourcesPanel( self, style=wx.SUNKEN_BORDER ) + self.pnlLog = LogPanel( self, style=wx.SUNKEN_BORDER ) + + # Build aui manager to hold all the widgets + self.BuildAuiManager() + + # Build menus and menu bar + self.BuildViewMenu() + self.BuildCreateMenu() + self.BuildWindowMenu() + self.BuildMenuBar() + + # Update the view menu based on the perspective saved in preferences + self.OnUpdateWindowMenu( None ) + + def _GetSavePath( self ): + + # Get default paths from current project directory, or the map's + # current location on disk + defaultDir = '' + defaultFile = '' + if self.app.doc.contents.filePath is not None: + defaultDir, defaultFile = os.path.split( self.app.doc.contents.filePath ) + elif self.app.project.path is not None: + defaultDir = self.app.project.GetMapsDirectory() + + # Open file browser + filePath = wxUtils.FileSaveDialog( 'Save Scene As', WILDCARD_SCENE, defaultDir=defaultDir, defaultFile=defaultFile ) + if filePath and os.path.exists( filePath ): + + # Warn user if the chosen file path already exists + msg = ''.join( ['The file "', filePath, '" already exists.\nDo you want to replace it?'] ) + if wxUtils.YesNoDialog( msg, 'Replace File?', wx.ICON_WARNING ) == wx.ID_NO: + return False + + return filePath + + def _CheckForSave( self ): + """ + If there is already a file loaded and it is dirty, query the user to + save the file. Return False for cancel, True otherwise. + """ + if self.app.doc.dirty: + + # Show dialog, record result + msg = ''.join( ['The document "', self.app.doc.title, '" was modified after last save.\nSave changes before continuing?'] ) + result = wxUtils.YesNoCancelDialog( msg, 'Save Changes?', wx.ICON_WARNING ) + if result == wx.ID_YES: + self.OnFileSave( None ) + elif result == wx.ID_CANCEL: + return False + + # Document not dirty, return True + return True + + def OnClose( self, evt ): + """Save frame and aui preferences, hide the window and quit.""" + # Check if ok to continue, stop the closing process if the user + # cancelled + if not self._CheckForSave(): + evt.Veto() + return + + # Save prefs, hide window and quit + self.auiCfg.Save() + if self.preMaxPos is not None: + self.auiCfg.SavePosition( *self.preMaxPos ) + if self.preMaxSize is not None: + self.auiCfg.SaveSize( *self.preMaxSize ) + if self.app.project.path is not None: + self.cfg.Write( 'projDirPath', self.app.project.path ) + self.Show( False ) + self.app.Quit() + + def OnFileNew( self, evt ): + """Show project settings panel and create new scene.""" + # Check if ok to continue, return if the user cancelled + if not self._CheckForSave(): + return + + # Create new document + self.app.CreateScene() + self.app.doc.OnRefresh() + + def OnFileOpen( self, evt, filePath=None ): + """Create a new document and load the scene.""" + # Check if ok to continue, return if the user cancelled + if not self._CheckForSave(): + return + + # Create new document from file path and load it + if filePath is None: + + # Get the start directory. This will be the current working + # directory if the project is not set. + mapsDirPath = self.app.project.GetMapsDirectory() + if mapsDirPath is None: + mapsDirPath = os.getcwd() + + filePath = wxUtils.FileOpenDialog( 'Open Scene', WILDCARD_SCENE, + defaultDir=mapsDirPath ) + + # Create new document + if filePath: + self.app.CreateScene( filePath ) + self.app.doc.Load() + + def OnFileSave( self, evt, saveAs=False ): + """Save the document.""" + # Set a file path for the document if one does not exist, or for save + # as + if self.app.doc.contents.filePath is None or saveAs: + + # Query a new save path + filePath = self._GetSavePath() + if filePath: + self.app.doc.contents.filePath = filePath + else: + return + + # Save the file + self.app.doc.Save() + + def OnFileSaveAs( self, evt ): + """ + Call save using the saveAs flag in order to bring up a new dialog box + so the user may set an alternate save path. + """ + self.OnFileSave( evt, True ) + + def OnFileImport( self, evt ): + """Import assets to project.""" + filePaths = wxUtils.FileOpenDialog( 'Import Models', WILDCARD_MODEL, + wx.MULTIPLE ) + if filePaths: + for filePath in filePaths: + self.app.project.ImportAsset( filePath ) + + def OnFileNewProject( self, evt ): + """Build project directory and set project.""" + dirPath = wxUtils.DirDialog( 'Set New Project Directory' ) + if dirPath: + self.app.project.New( dirPath ) + self.SetProjectPath( dirPath ) + self.app.doc.OnRefresh() + + def OnFileSetProject( self, evt ): + """ + Set the active project directory path and rebuild the resources panel. + """ + dirPath = wxUtils.DirDialog( 'Set Project Directory' ) + if dirPath: + self.SetProjectPath( dirPath ) + self.app.doc.OnRefresh() + + def OnFileBuildProject( self, evt ): + """Build the current project to a p3d file.""" + filePath = wxUtils.FileSaveDialog( 'Build Project', WILDCARD_P3D ) + if filePath: + self.app.project.Build( filePath ) + + def OnUndo( self, evt ): + self.app.actnMgr.Undo() + + def OnRedo( self, evt ): + self.app.actnMgr.Redo() + + def OnViewGrid( self, evt ): + """ + Show or hide the grid based on the checked value of the menu item. + """ + checked = evt.Checked() + if checked: + self.app.grid.show() + else: + self.app.grid.hide() + + def OnCreate( self, evt, typeStr ): + """Create the node path, add it to the scene and select it.""" + node = base.game.nodeMgr.Create( typeStr ) + np = pm.NodePath( node ) + cmds.Add( [np] ) + + def OnCreateActor( self, evt ): + """ + Turn the selection into actors. This is still a massive hack - we need + a more concise way of storing this information. + """ + actors = [] + for np in self.app.selection.nps: + + # Get the panda object for this node path or create one if it + # doesn't exist + pObj = p3d.PandaObject.Get( np ) + if pObj is None: + pObj = p3d.PandaObject( np ) + + # Create an actor from the node path + pObj.CreateActor() + + # HAXX - this could end up being pretty dodgy. We need a list of + # node paths in order to set the selection - not a list of actors. + # This seems like a somewhat reliable method of getting that. + np = pObj.np.anyPath( pObj.np.node() ) + self.app.doc.contents.AddNodePaths( [np] ) + actors.append( np ) + + # DEBUG + # Set actor type + np.setTag( game.nodes.TAG_NODE_TYPE, 'Actor' ) + + self.app.Remove( self.app.selection.nps ) + self.app.Select( actors ) + + def OnCreatePrefab( self, evt ): + """ + Create a new prefab for the selected object in the prefab directory. + """ + np = self.app.selection.nps[0] + dirPath = self.app.project.GetPrefabsDirectory() + assetName = self.app.project.GetUniqueFileName( 'prefab.xml', os.listdir( dirPath ) ) + assetPath = os.path.join( dirPath, assetName ) + self.app.scene.parser.Save( np, assetPath ) + + def OnCreateCgShader( self, evt ): + """ + + """ + self.app.project.CreateCgShader() + + def OnCreateGlslShader( self, evt ): + """ + + """ + self.app.project.CreateGlslShader() + + def OnShowHidePane( self, evt ): + """ + Show or hide the pane based on the menu item that was (un)checked. + """ + pane = self.paneDefs[evt.GetId()][0] + self._mgr.GetPane( pane ).Show( evt.Checked() ) + + # Make sure to call or else we won't see any changes + self._mgr.Update() + + def OnUpdateWindowMenu( self, evt ): + """ + Set the checks in the window menu to match the visibility of the + panes. + """ + def UpdateWindowMenu(): + + # Check those menus representing panels which are still shown + # after the event + for id in self.paneDefs: + pane = self.paneDefs[id][0] + if self.mWind.FindItemById( id ) and self._mgr.GetPane( pane ).IsShown(): + self.mWind.Check( id, True ) + + # Uncheck all menus + for id in self.paneDefs: + if self.mWind.FindItemById( id ): + self.mWind.Check( id, False ) + + # Call after or IsShown() won't return a useful value + wx.CallAfter( UpdateWindowMenu ) + + def OnUpdate( self, msg ): + """ + Change the appearance and states of buttons on the form based on the + state of the loaded document. + + NOTE: Don't use freeze / thaw as this will cause the 3D viewport to + flicker. + """ + # Check the grid menu item if the grid is shown + self.mView.Check( ID_VIEW_GRID, False ) + if not self.app.grid.isHidden(): + self.mView.Check( ID_VIEW_GRID, True ) + + # Disable all tools + self.mFile.EnableAllTools( False ) + self.tbFile.EnableAllTools( False ) + self.mProj.EnableAllTools( False ) + + # Turn some tools back on depending on editor state. + self.mFile.Enable( ID_FILE_NEW, True ) + self.mFile.Enable( ID_FILE_OPEN, True ) + self.mFile.Enable( ID_FILE_SAVE_AS, True ) + self.mFile.Enable( ID_FILE_PROJ, True ) + self.tbFile.EnableTool( ID_FILE_NEW, True ) + self.tbFile.EnableTool( ID_FILE_OPEN, True ) + self.tbFile.EnableTool( ID_FILE_SAVE_AS, True ) + self.mProj.Enable( ID_PROJ_NEW, True ) + self.mProj.Enable( ID_PROJ_SET, True ) + if self.app.doc.dirty: + self.mFile.Enable( ID_FILE_SAVE, True ) + self.tbFile.EnableTool( ID_FILE_SAVE, True ) + if self.app.project.path is not None: + self.mFile.Enable( ID_FILE_IMPORT, True ) + self.mProj.EnableAllTools( True ) + self.tbFile.Refresh() + + # Set the frame's title to include the document's file path, include + # dirty 'star' + title = ''.join( [FRAME_TITLE, ' - ', self.app.doc.title] ) + if self.app.doc.dirty: + title += ' *' + self.SetTitle( title ) + + self.app.game.pluginMgr.OnUpdate( msg ) + + def OnMove( self, evt ): + """ + Keep the window's position on hand before it gets maximized as this is + the number we need to save to preferences. + """ + if not self.IsMaximized(): + self.preMaxPos = self.GetPosition() + + def OnSize( self, evt ): + """ + Keep the window's size on hand before it gets maximized as this is the + number we need to save to preferences. + """ + if not self.IsMaximized(): + self.preMaxSize = self.GetSize() + + def SetProjectPath( self, dirPath ): + """ + Set the project path and rebuild the resources panel. + """ + self.app.project.Set( dirPath ) + self.pnlRsrcs.Build( self.app.project.path ) + + def BuildFileActions( self ): + """Add tools, set long help strings and bind toolbar events.""" + commonActns = [ + ActionItem( 'New', os.path.join( 'data', 'images', 'document.png' ), self.OnFileNew, ID_FILE_NEW ), + ActionItem( 'Open', os.path.join( 'data', 'images', 'folder-horizontal-open.png' ), self.OnFileOpen, ID_FILE_OPEN ), + ActionItem( 'Save', os.path.join( 'data', 'images', 'disk-black.png' ), self.OnFileSave, ID_FILE_SAVE ), + ActionItem( 'Save As', os.path.join( 'data', 'images', 'disk-black-pencil.png' ), self.OnFileSaveAs, ID_FILE_SAVE_AS ), + ] + + # Create file menu + self.mFile = CustomMenu() + self.mFile.AppendActionItems( commonActns, self ) + self.mFile.AppendSeparator() + self.mFile.AppendActionItem( ActionItem( 'Import...', '', self.OnFileImport, ID_FILE_IMPORT ), self ) + + # Create project actions as a submenu + self.mProj = CustomMenu() + actns = [ + ActionItem( 'New...', '', self.OnFileNewProject, ID_PROJ_NEW ), + ActionItem( 'Set...', '', self.OnFileSetProject, ID_PROJ_SET ), + ActionItem( 'Build...', '', self.OnFileBuildProject, ID_PROJ_BUILD ) + ] + self.mProj.AppendActionItems( actns, self ) + self.mFile.AppendMenu( ID_FILE_PROJ, '&Project', self.mProj ) + + # Create file toolbar + self.tbFile = CustomAuiToolBar( self, -1, style=wx.aui.AUI_TB_DEFAULT_STYLE ) + self.tbFile.SetToolBitmapSize( TBAR_ICON_SIZE ) + self.tbFile.AppendActionItems( commonActns ) + self.tbFile.Realize() + + def BuildEditActions( self ): + """Add tools, set long help strings and bind toolbar events.""" + actns = [ + ActionItem( 'Undo', os.path.join( 'data', 'images', 'arrow-curve-flip.png' ), self.OnUndo ), + ActionItem( 'Redo', os.path.join( 'data', 'images', 'arrow-curve.png' ), self.OnRedo ) + ] + + # Create edit menu + self.mEdit = CustomMenu() + self.mEdit.AppendActionItems( actns, self ) + + # Create edit toolbar + self.tbEdit = CustomAuiToolBar( self, -1, style=wx.aui.AUI_TB_DEFAULT_STYLE ) + self.tbEdit.SetToolBitmapSize( TBAR_ICON_SIZE ) + self.tbEdit.AppendActionItems( actns ) + self.tbEdit.Realize() + + def BuildViewMenu( self ): + """Build the view menu.""" + viewActns = [ + ActionItem( 'Grid', '', self.OnViewGrid, ID_VIEW_GRID, check=True ) + ] + self.mView = CustomMenu() + self.mView.AppendActionItems( viewActns, self ) + + def BuildCreateMenu( self ): + """Build the create menu.""" + lightActns = [ + ActionItem( 'Ambient Light', '', self.OnCreate, args='AmbientLight' ), + ActionItem( 'Point Light', '', self.OnCreate, args='PointLight' ), + ActionItem( 'Directional Light', '', self.OnCreate, args='DirectionalLight' ), + ActionItem( 'Spotlight', '', self.OnCreate, args='Spotlight' ) + ] + mLights = CustomMenu() + mLights.AppendActionItems( lightActns, self ) + + self.mCreate = CustomMenu() + self.mCreate.AppendActionItem( ActionItem( 'Panda Node', '', self.OnCreate, args='PandaNode' ), self ) + #self.mCreate.AppendActionItem( ActionItem( 'Actor', '', self.OnCreateActor ), self ) # Not supported... yet. + self.mCreate.AppendSubMenu( mLights, '&Lights' ) + #self.mCreate.AppendSeparator() + #self.mCreate.AppendActionItem( ActionItem( 'Collision Node', '', self.OnCreate, args='CollisionNode' ), self ) + #self.mCreate.AppendSeparator() + #self.mCreate.AppendActionItem( ActionItem( 'Prefab', '', self.OnCreatePrefab ), self ) + #self.mCreate.AppendSeparator() + #self.mCreate.AppendActionItem( ActionItem( 'Cg Shader', '', self.OnCreateCgShader ), self ) + #self.mCreate.AppendActionItem( ActionItem( 'Glsl Shader', '', self.OnCreateGlslShader ), self ) + + def BuildWindowMenu( self ): + """Build show / hide controls for panes.""" + self.mWind = wx.Menu() + for id, paneDef in self.paneDefs.items(): + if paneDef[1]: + self.mWind.AppendCheckItem( id, paneDef[2].caption ) + self.Bind( wx.EVT_MENU, self.OnShowHidePane, id=id ) + + def BuildMenuBar( self ): + """Build the meny bar and attach all menus to it.""" + self.mb = wx.MenuBar() + self.mb.Append( self.mFile, '&File' ) + self.mb.Append( self.mEdit, '&Edit' ) + self.mb.Append( self.mView, '&View' ) + self.mb.Append( self.mCreate, '&Create' ) + self.mb.Append( self.mWind, '&Window' ) + self.SetMenuBar( self.mb ) + + def BuildAuiManager( self ): + """ + Define the behaviour for each aui manager panel, then add them to the + manager. + """ + # Define aui manager panes + # Each tuple is defined as: widget, show in window menu, aui panel + # info + self.paneDefs = { + ID_WIND_FILE_TOOLBAR:(self.tbFile, True, + wx.aui.AuiPaneInfo() + .Name( 'tbFile' ) + .Caption( 'File Toolbar' ) + .ToolbarPane() + .Top()), + + ID_WIND_EDIT_TOOLBAR:(self.tbEdit, True, + wx.aui.AuiPaneInfo() + .Name( 'tbEdit' ) + .Caption( 'Edit Toolbar' ) + .ToolbarPane() + .Top()), + + ID_WIND_VIEWPORT:(self.nbViewport, True, + wx.aui.AuiPaneInfo() + .Name( 'pnlGameView' ) + .Caption( 'Viewport' ) + .CloseButton( False ) + .MaximizeButton( True ) + .Center()), + + ID_WIND_SCENE_GRAPH:(self.pnlSceneGraph, True, + wx.aui.AuiPaneInfo() + .Name( 'pnlSceneGraph' ) + .Caption( 'Scene Graph' ) + .CloseButton( True ) + .MaximizeButton( True ) + .MinSize( (100, 100) ) + .Left() + .Position( 2 )), + + ID_WIND_PROPERTIES:(self.pnlProps, True, + wx.aui.AuiPaneInfo() + .Name( 'pnlProps' ) + .Caption( 'Properties' ) + .CloseButton( True ) + .MaximizeButton( True ) + .MinSize( (100, 100) ) + .Right()), + + ID_WIND_RESOURCES:(self.pnlRsrcs, True, + wx.aui.AuiPaneInfo() + .Name( 'pnlRsrcs' ) + .Caption( 'Resources' ) + .CloseButton( True ) + .MaximizeButton( True ) + .MinSize( (100, 100) ) + .Right() + .Position( 2 )), + + ID_WIND_LOG:(self.pnlLog, True, + wx.aui.AuiPaneInfo() + .Name( 'pnlLog' ) + .Caption( 'Log' ) + .CloseButton( True ) + .MaximizeButton( True ) + .MinSize( (100, 100) ) + .Bottom() + .Position( 1 )) + } + + # Build aui manager and add each pane + self._mgr = wx.aui.AuiManager( self ) + for paneDef in self.paneDefs.values(): + self._mgr.AddPane( paneDef[0], paneDef[2] ) + + # Bind aui manager events + self._mgr.Bind( wx.aui.EVT_AUI_PANE_CLOSE, self.OnUpdateWindowMenu ) + + # Create config and load preferences for all panels + self.auiCfg = AuiManagerConfig( self._mgr, 'pandaEditorWindow' ) + self.auiCfg.Load() + self._mgr.Update() \ No newline at end of file diff --git a/src/pandaEditor/ui/projectSettingsPanel.py b/src/pandaEditor/ui/projectSettingsPanel.py new file mode 100644 index 0000000..15082c8 --- /dev/null +++ b/src/pandaEditor/ui/projectSettingsPanel.py @@ -0,0 +1,31 @@ +import wx + + +class ProjectSettingsPanel( wx.Panel ): + + def __init__( self, *args, **kwargs ): + wx.Panel.__init__( self, *args, **kwargs ) + + # Project directory + bs1 = wx.BoxSizer( wx.HORIZONTAL ) + bs1.Add( wx.StaticText( self, -1, 'Project Directory:' ), 1, wx.RIGHT, 10 ) + bs1.Add( wx.TextCtrl( self, -1, '', size=(125, -1) ), 1, wx.RIGHT, 10 ) + bs1.Add( wx.Button( self, -1 ), 1 ) + + # Project name + bs2 = wx.BoxSizer( wx.HORIZONTAL ) + bs2.Add( wx.StaticText( self, -1, 'Project Name:' ), 1, wx.RIGHT, 10 ) + bs2.Add( wx.TextCtrl( self, -1, '', size=(125, -1) ), 1, wx.RIGHT, 10 ) + bs2.Add( wx.Button( self, -1, '...' ), 1 ) + + # Build sizers + bs = wx.BoxSizer( wx.VERTICAL ) + bs.Add( bs1, 0 ) + bs.Add( bs2, 0 ) + + # Build border + self.border = wx.BoxSizer( wx.VERTICAL ) + self.border.Add( bs, 1, wx.ALL, 10 ) + self.SetSizer( self.border ) + + #def BuildFieldWithLabel( self, text, label ) \ No newline at end of file diff --git a/src/pandaEditor/ui/propertiesPanel.py b/src/pandaEditor/ui/propertiesPanel.py new file mode 100644 index 0000000..3aeab70 --- /dev/null +++ b/src/pandaEditor/ui/propertiesPanel.py @@ -0,0 +1,311 @@ +import wx +from wxExtra import wxpg +from wx.lib.pubsub import Publisher as pub +import pandac.PandaModules as pm + +from .. import commands as cmds +import customProperties as custProps + + +ATTRIBUTE_TAG = 'attr' + + +class PropertyGrid( wxpg.PropertyGrid ): + + """ + Unfortunately I've had to override some of the basic methods of the + property grid in order to overcome what seems like an odd limitation / + feature. When calling GetProperty() the grid sends back the base class + PGProperty, not the actual class that was used when we initially called + Append(). This seems odd to me as then none of the overridden methods used + in custom properties will work... + """ + + def __init__( self, *args, **kwargs ): + wxpg.PropertyGrid.__init__( self, *args, **kwargs ) + + self._propsByName = {} + self._propsByLabel = {} + self._propsByLongLabel = {} + + def Append( self, prop ): + + # Do not allow properties with '|' in the label as we use this as a + # delimiter + if '|' in prop.GetLabel(): + msg = ( 'Cannot use property labels containing the pipe (\'|\')' + + 'character' ) + raise AttributeError, msg + + # Add the property + wxpg.PropertyGrid.Append( self, prop ) + + # Store the property again in our dicts by name and label + self._propsByName[prop.GetName()] = prop + self._propsByLabel[prop.GetLabel()] = prop + + # Store the property plus all its children in the long label dict + allProps = self._propsByLongLabel + allChildren = self.GetAllChildren( prop ) + self._propsByLongLabel = dict( allProps.items() + allChildren.items() ) + + def Clear( self ): + + # Empty property dicts before using default clear method + self._propsByName = {} + self._propsByLabel = {} + self._propsByLongLabel = {} + + wxpg.PropertyGrid.Clear( self ) + + def GetPropertyByName( self, name ): + + # Return value from the property dict + if name in self._propsByName: + return self._propsByName[name] + + return None + + def GetPropertyByLabel( self, label ): + + # Return value from the property dict + if label in self._propsByLabel: + return self._propsByLabel[label] + + return None + + def GetPropertyByLongLabel( self, longLbl ): + """ + Return the property from the property dict matching the indicated + long label. + """ + if longLbl in self._propsByLongLabel: + return self._propsByLongLabel[longLbl] + + return None + + def GetPropertyLongLabel( self, prop ): + """ + Return the property's long label. Do this by iterating to the top of + the hierarchy and joining each parent's label with a pipe character + until there are no more parents. + """ + elem = [] + + while True: + if prop.GetParent() is None: + break + else: + elem.insert( 0, prop.GetLabel() ) + prop = prop.GetParent() + + return '|'.join( elem ) + + + def GetAllChildren( self, prop, parentLbl=None ): + """ + Return all decendant properties of the indicated property. + """ + result = {} + + # Use property parent label if None is supplied + if parentLbl is None: + parentLbl = prop.GetParent().GetLabel() + + # Get the long label for this property + lblElems = [] + if parentLbl: + lblElems.append( parentLbl ) + lblElems.append( prop.GetLabel() ) + longLbl = '|'.join( lblElems ) + + # Add the property to the dictionary + result[longLbl] = prop + + # Recurse down hierarchy + for i in range( prop.GetCount() ): + result = dict( result.items() + self.GetAllChildren( prop.Item( i ), longLbl ).items() ) + + return result + + def GetProperties( self ): + + # Return values of the property dict + return self._propsByName.values() + + def GetPropertiesDictionary( self ): + props = {} + + # Include children if specified + for propLabel, prop in self._propsByLabel.items(): + + # Ignore property categories + if prop.IsCategory(): + props[prop.GetLabel()] = prop + continue + + childs = self.GetAllChildren( prop ) + props = dict( props.items() + childs.items() ) + + # Return values of the property dict + return props + + def Enable( self, value ): + """ + Overridden from wxpg.PropertyGrid. A disabled property grid doesn't + seem to change in its appearance. Grey out all properties to give a + nice visual indication of the state of the panel. + """ + wxpg.PropertyGrid.Enable( self, value ) + + # Remove the selection if we are disabling the panel + if not value: + self.SetSelection( [] ) + + # Grey out all properties if we are disabling the panel + for property in self.GetProperties(): + if value: + self.SetPropertyColourToDefault( property ) + else: + self.SetPropertyTextColour( property, wx.Colour(150, 150, 150) ) + + +class PropertiesPanel( wx.Panel ): + + def __init__( self, *args, **kwargs ): + wx.Panel.__init__( self, *args, **kwargs ) + + self.propExps = {} + + # Define how each type of value should be edited + self.propMap = { + None:wxpg.PropertyCategory, + pm.Vec2:custProps.Float2Property, + pm.Vec3:custProps.Float3Property, + pm.Vec4:custProps.Float4Property, + pm.Point2:custProps.Point2Property, + pm.Point3:custProps.Point3Property, + pm.Point4:custProps.Point4Property, + pm.NodePath:custProps.NodePathProperty, + int:wxpg.IntProperty, + str:wxpg.StringProperty, + bool:wxpg.BoolProperty, + float:wxpg.FloatProperty + } + + # Bind publisher events + pub.subscribe( self.OnUpdate, 'Update' ) + pub.subscribe( self.OnUpdate, 'UpdateSelection' ) + pub.subscribe( self.OnSelectionModified, 'SelectionModified' ) + + # Build property grid + self.pg = PropertyGrid( self ) + + # Bind property grid events + self.pg.Bind( wxpg.EVT_PG_CHANGED, self.OnPgChanged ) + + # Build sizers + self.bs1 = wx.BoxSizer( wx.VERTICAL ) + self.bs1.Add( self.pg, 1, wx.EXPAND ) + self.SetSizer( self.bs1 ) + + def BuildPropertyGrid( self ): + """ + Build the properties for the grid based on the contents of nps. + """ + self.pg.Clear() + + # For convenience + nps = wx.GetApp().selection.nps + if not nps: + return + + def RecurseAttribute( attr, parent=None ): + + # Find the correct property to display this attribute + if attr.type in self.propMap: + prop = None + if attr.e: + propCls = self.propMap[attr.type] + if attr.type is not None: + prop = propCls( attr.label, '', attr.Get( nps[0] ) ) + prop.SetAttribute( ATTRIBUTE_TAG, attr ) + else: + prop = propCls( attr.label ) + nextP = prop + else: + nextP = parent + + # Recurse through attribute children. + for child in attr.children: + RecurseAttribute( child, nextP ) + + if prop is None: + return + + # Append to property grid or the last property if it is a + # child. + #if parent is None: + self.pg.Append( prop ) + #else: + # parent.AddPrivateChild( prop ) + + # Build all properties from attributes. + wrpr = base.game.nodeMgr.Wrap( nps[0] ) + if wrpr is not None: + for attr in wrpr.GetAttributes(): + RecurseAttribute( attr ) + + def OnPgChanged( self, evt ): + """ + Set the node path or node's property using the value the user entered + into the grid. + """ + # Should probably never get here... + nps = wx.GetApp().selection.nps + if not nps: + return + + # Get the node property from the property and set it. + prop = evt.GetProperty() + attr = prop.GetAttribute( ATTRIBUTE_TAG ) + + cmds.SetAttribute( nps, attr, prop.GetValue() ) + + def OnUpdate( self, msg ): + self.pg.Freeze() + + # Get property expanded states + allProps = self.pg.GetPropertiesDictionary() + for propLongLbl, prop in allProps.items(): + self.propExps[propLongLbl] = prop.IsExpanded() + + # Clear and rebuild property grid + self.BuildPropertyGrid() + + # Set expanded states back + for propLongLbl, expanded in self.propExps.items(): + prop = self.pg.GetPropertyByLongLabel( propLongLbl ) + if prop is not None: + prop.SetExpanded( expanded ) + + self.pg.Thaw() + + def OnSelectionModified( self, msg ): + """ + Update the position, rotation and scale properties during the + transform operation. + """ + np = msg.data[0] + labelFnDict = { + 'Position':np.getPos, + 'Rotation':np.getHpr, + 'Scale':np.getScale + } + + # Set the value of each property to the result returned from calling + # the function. + for label, fn in labelFnDict.items(): + prop = self.pg.GetPropertyByLabel( label ) + if prop is not None: + prop.SetValue( fn() ) \ No newline at end of file diff --git a/src/pandaEditor/ui/resourcesPanel.py b/src/pandaEditor/ui/resourcesPanel.py new file mode 100644 index 0000000..737d713 --- /dev/null +++ b/src/pandaEditor/ui/resourcesPanel.py @@ -0,0 +1,134 @@ +import os + +import wx +from wx.lib.pubsub import Publisher as pub + +import p3d +from wxExtra import DirTreeCtrl + + +class ResourcesPanel( wx.Panel ): + + def __init__( self, *args, **kwargs ): + wx.Panel.__init__( self, *args, **kwargs ) + + # Bind project file events + pub.subscribe( self.OnUpdate, 'projectFilesAdded' ) + pub.subscribe( self.OnUpdate, 'projectFilesRemoved' ) + + # Build sizers + self.bs1 = wx.BoxSizer( wx.VERTICAL ) + self.SetSizer( self.bs1 ) + + def Build( self, projDirPath ): + + # Clear all widgets from the sizer + self.bs1.Clear( True ) + if projDirPath is not None and os.path.isdir( projDirPath ): + + # Build tree control and add it to the sizer + self.dtc = DirTreeCtrl( self, -1, style= + wx.NO_BORDER | + wx.TR_DEFAULT_STYLE | + wx.TR_EDIT_LABELS ) + self.dtc.SetRootDir( projDirPath ) + self.dtc.Expand( self.dtc.GetRootItem() ) + self.bs1.Add( self.dtc, 1, wx.EXPAND ) + + # Bind tree control events + self.dtc.Bind( wx.EVT_KEY_UP, p3d.wx.OnKeyUp ) + self.dtc.Bind( wx.EVT_KEY_DOWN, p3d.wx.OnKeyDown ) + self.dtc.Bind( wx.EVT_LEFT_UP, p3d.wx.OnLeftUp ) + self.dtc.Bind( wx.EVT_MIDDLE_DOWN, self.OnMiddleDown ) + self.dtc.Bind( wx.EVT_LEFT_DCLICK, self.OnLeftDClick ) + self.dtc.Bind( wx.EVT_TREE_END_LABEL_EDIT, self.OnTreeEndLabelEdit ) + else: + + # Build and display "project not set" warning + tc = wx.StaticText( self, -1, 'Project directory not set', style=wx.ALIGN_CENTER ) + font = tc.GetFont() + font.SetWeight( wx.FONTWEIGHT_BOLD ) + tc.SetFont( font ) + self.bs1.AddSpacer( 10 ) + self.bs1.Add( tc, 1, wx.EXPAND | wx.LEFT | wx.RIGHT, 2 ) + + self.bs1.Layout() + + def OnMiddleDown( self, evt ): + + # Get the item under the mouse - bail if the item is not ok + itemId = self.dtc.HitTest( wx.Point( evt.GetX(), evt.GetY() ) )[0] + if itemId is None or not itemId.IsOk(): + return + + # Select it + self.dtc.SelectItem( itemId ) + + # Create a custom data object that contains the full path of the item + do = wx.CustomDataObject( 'filePath' ) + do.SetData( str( self.dtc.GetItemPath( itemId ) ) ) + + # Create the drop source and begin the drag and drop operation + ds = wx.DropSource( self ) + ds.SetData( do ) + ds.DoDragDrop( wx.Drag_AllowMove ) + + def OnLeftDClick( self, evt ): + + # Load items + itemId = self.dtc.HitTest( wx.Point( evt.GetX(), evt.GetY() ) )[0] + filePath = self.dtc.GetItemPath( itemId ) + ext = os.path.splitext( os.path.basename( self.dtc.GetItemText( itemId ) ) )[1] + if ext == '.xml': + wx.GetApp().frame.OnFileOpen( None, filePath ) + elif ext == '.py': + os.startfile( filePath ) + + def OnTreeEndLabelEdit( self, evt ): + """Change the name of the asset in the system.""" + # Bail if no valid label is entered + if not evt.GetLabel(): + evt.Veto() + return + + # Construct new file path and rename + oldPath = self.dtc.GetItemPath( evt.GetItem() ) + head, tail = os.path.split( oldPath ) + newPath = os.path.join( head, evt.GetLabel() ) + os.rename( oldPath, newPath ) + + def OnUpdate( self, arg ): + """Rebuild the directory tree.""" + self.dtc.Freeze() + + def GetExpandedDict(): + + # Return a dictionary mapping each node path to its tree item. + expDict = {} + for item in self.dtc.GetAllItems(): + dir = self.dtc.GetPyData( item ) + if dir is not None: + expDict[dir.directory] = (item, self.dtc.IsExpanded( item )) + return expDict + + # Get map of directory paths to items and expanded states before + # updating + oldItems = GetExpandedDict() + + # Rebuild the tree control + rootItem = self.dtc.GetRootItem() + rootDirPath = self.dtc.GetPyData( rootItem ).directory + self.dtc._loadDir( rootItem, rootDirPath ) + + # Get map of directory paths to items and expanded states after + # updating + newItems = GetExpandedDict() + + # Set the expanded states back + for dirPath, grp in oldItems.items(): + oldItem, oldExp = grp + if dirPath in newItems and oldExp: + newItem, newExp = newItems[dirPath] + self.dtc.Expand( newItem ) + + self.dtc.Thaw() diff --git a/src/pandaEditor/ui/sceneGraphBasePanel.py b/src/pandaEditor/ui/sceneGraphBasePanel.py new file mode 100644 index 0000000..ed0f316 --- /dev/null +++ b/src/pandaEditor/ui/sceneGraphBasePanel.py @@ -0,0 +1,228 @@ +import wx +import wx.lib.agw.customtreectrl as ct +from wx.lib.pubsub import Publisher as pub + +import p3d +from .. import commands as cmds +from wxExtra import CustomTreeCtrl, CompositeDropTarget + + +class SceneGraphBasePanel( wx.Panel ): + + def __init__( self, *args, **kwargs ): + wx.Panel.__init__( self, *args, **kwargs ) + + self._updating = False + self._selUpdating = False + self.dragNps = [] + + # Build tree control + self.tc = CustomTreeCtrl( self, -1, agwStyle= + ct.TR_EDIT_LABELS | + ct.TR_HIDE_ROOT | + ct.TR_FULL_ROW_HIGHLIGHT | + ct.TR_NO_LINES | + ct.TR_HAS_BUTTONS | + ct.TR_TWIST_BUTTONS | + ct.TR_MULTIPLE ) + self.tc.AddRoot( 'root' ) + + # Bind tree control events + self.tc.Bind( wx.EVT_TREE_END_LABEL_EDIT, self.OnTreeEndLabelEdit ) + self.tc.Bind( wx.EVT_TREE_ITEM_ACTIVATED, self.OnTreeItemActivated ) + self.tc.Bind( wx.EVT_KEY_UP, p3d.wx.OnKeyUp ) + self.tc.Bind( wx.EVT_KEY_DOWN, p3d.wx.OnKeyDown ) + self.tc.Bind( wx.EVT_LEFT_UP, p3d.wx.OnLeftUp ) + self.tc.Bind( wx.EVT_MIDDLE_DOWN, self.OnMiddleDown ) + + # Build tree control drop target + self.dt = CompositeDropTarget( ['filePath', 'nodePath'], + self.OnDropItem, + self.ValidateDropItem ) + self.tc.SetDropTarget( self.dt ) + + # Bind publisher events + pub.subscribe( self.OnUpdate, 'Update' ) + + # Build sizers + self.bs1 = wx.BoxSizer( wx.VERTICAL ) + self.bs1.Add( self.tc, 1, wx.EXPAND ) + self.SetSizer( self.bs1 ) + + def OnTreeEndLabelEdit( self, evt ): + """Match the node path's name to the new name of the item.""" + def setNodePathName( np, name ): + np.setName( name ) + wx.CallAfter( wx.GetApp().doc.OnModified ) + np = evt.GetItem().GetData() + name = evt.GetLabel() + if not name: + return + wx.CallAfter( setNodePathName, np, name ) + + def OnTreeItemActivated( self, evt ): + """Put the event item into label edit mode.""" + self.tc.EditLabel( evt.GetItem() ) + + def OnMiddleDown( self, evt ): + + # Get the item under the mouse - bail if the item is bad + item = self.tc.HitTest( wx.Point( evt.GetX(), evt.GetY() ) )[0] + if item is None or not item.IsOk(): + return + + # Create a custom data object that we can drop onto the toolbar + # which contains the tool's id as a string + do = wx.CustomDataObject( 'NodePath' ) + do.SetData( str( item.GetData() ) ) + + # If the item under the middle mouse click is part of the selection + # then use the whole selection, otherwise just use the item. + if item.GetData() in wx.GetApp().selection.nps: + self.dragNps = wx.GetApp().selection.nps + else: + self.dragNps = [item.GetData()] + + # Create the drop source and begin the drag and drop operation + ds = wx.DropSource( self ) + ds.SetData( do ) + ds.DoDragDrop( wx.Drag_AllowMove ) + + # Clear drag node paths + self.dragNps = [] + + def ValidateDropItem( self, x, y ): + """Perform validation procedures.""" + dropItem = ( self.tc.HitTest( wx.Point( x, y ) ) )[0] + + # If the drop item is none then the drop item will default to the + # root node. No other checks necessary. + if dropItem is None: + return True + + # Fail if the drop item is one of the items being dragged + dropNp = dropItem.GetData() + if dropNp in self.dragNps: + return False + + # Fail if the drag items are ancestors of the drop items + for np in self.dragNps: + if np.isAncestorOf( dropNp ): + return False + + # Drop target item is ok, continue + return True + + def OnDropItem( self, str ): + + # Get the item at the drop point + dropItem = ( self.tc.HitTest( wx.Point( self.dt.x, self.dt.y ) ) )[0] + + # Check if there are drag node paths set, if so perform parenting + # operation + if not self.dragNps: + np = None + if dropItem is not None: + np = dropItem.GetData() + wx.GetApp().AddFile( str, np ) + return + + # Get the parent + if dropItem is not None and dropItem.IsOk(): + parentNp = dropItem.GetData() + else: + parentNp = wx.GetApp().doc.contents.rootNp + + # Parent all dragged node paths + cmds.Parent( self.dragNps, parentNp ) + + def PopulateTreeControl( self, doc ): + """ + Traverse the scene from the root node, creating tree items for each + node path encountered. + """ + def AddItem( np, parentItem ): + if np is doc.contents.rootNp: + return + if np.getParent() in self._nps: + parentItem = self._nps[np.getParent()] + else: + parentItem = self.tc.GetRootItem() + item = self.tc.AppendItem( parentItem, np.getName() ) + item.SetData( np ) + self._nps[np] = item + + # Clear the node path / tree item dict + self._nps = {} + + # Create scene root node, then recurse down scene hierarchy + self.tc.AddRoot( 'root' ) + wx.GetApp().doc.contents.Walk( AddItem, includeHelpers=False, modelRootsOnly=False ) + + def OnUpdate( self, msg ): + """ + Update the tree control by removing all items and replacing them. + """ + self._updating = True + + def GetItemsDict(): + + # Return a dictionary mapping each node path to its tree item. + itemsDict = {} + for item in self.tc.GetAllItems(): + itemsDict[item.GetData()] = item + return itemsDict + + self.tc.Freeze() + + # Get map of node paths to items before populating the tree control + oldItems = GetItemsDict() + + # Clear existing items and repopulate tree control + self.tc.DeleteAllItems() + self.PopulateTreeControl( msg.data ) + + # Get map of node paths to items after populating the tree control + newItems = GetItemsDict() + + # Set item states back + sels = [] + for np, oldItem in oldItems.items(): + + # Set expanded states back + if np in newItems and oldItem.IsExpanded(): + self.tc.Expand( newItems[np] ) + + # Set selection states back + if np in newItems and oldItem.IsSelected(): + self.tc.SelectItem( newItems[np] ) + #sels.append( newItems[np] ) + + #self.SelectItems( sels ) + + self.tc.Thaw() + + self._updating = False + + def SelectItems( self, items, unselect=True ): + """ + The tree control tries to redraw every time we call SelectItem() which + can cause flickering (even when frozen!) when iterating through a + list. Disconnecting the event handler seems to stop the internal + redrawing, at least until we get a SelectItems() method. + """ + self.tc.SetEvtHandlerEnabled( False ) + + # Deselect all if indicated + if unselect: + self.tc.UnselectAll() + + # Iterate over list and select + for item in items: + self.tc.SelectItem( item ) + + self.tc.SetEvtHandlerEnabled( True ) + + # Make sure to call refresh at least once since we disabled the event + # handler that would normally do this! + self.tc.Refresh() \ No newline at end of file diff --git a/src/pandaEditor/ui/sceneGraphPanel.py b/src/pandaEditor/ui/sceneGraphPanel.py new file mode 100644 index 0000000..4c6209b --- /dev/null +++ b/src/pandaEditor/ui/sceneGraphPanel.py @@ -0,0 +1,73 @@ +import wx +from wx.lib.pubsub import Publisher as pub + +from .. import commands as cmds +from sceneGraphBasePanel import SceneGraphBasePanel + + +class SceneGraphPanel( SceneGraphBasePanel ): + + def __init__( self, *args, **kwargs ): + SceneGraphBasePanel.__init__( self, *args, **kwargs ) + + # Bind tree control events + self.tc.Bind( wx.EVT_TREE_SEL_CHANGED, self.OnTreeSelChanged ) + + # Bind publisher events + pub.subscribe( self.OnUpdateSelection, 'UpdateSelection' ) + + def OnTreeSelChanged( self, evt ): + """ + Tree item selection handler. If the selection of the tree changes, + tell the app to select those node paths. + """ + # Bail if we got here by setting the item inside the OnUpdateSelection + # method, we get stuck in an infinite loop otherwise + if self._selUpdating or self._updating: + return + + # Get all valid selected items + items = [] + for item in self.tc.GetSelections(): + if item.IsOk() and item is not self.tc.GetRootItem(): + items.append( item ) + + # Set selected items + if items: + nps = [item.GetData() for item in items] + cmds.Select( nps ) + + """ + + def OnUpdate( self, msg ): + SceneGraphBasePanel.OnUpdate( self, msg ) + + def GetItemsDict(): + + # Return a dictionary mapping each node path to its tree item. + itemsDict = {} + for item in self.tc.GetAllItems(): + itemsDict[item.GetData()] = item + return itemsDict + + # Set the selection back + newItems = GetItemsDict() + selItems = [ + newItems[np] for np in wx.GetApp().selection.nps + if np in newItems + ] + self.SelectItems( selItems ) + """ + + def OnUpdateSelection( self, msg ): + """ + Select those items which correlate to the selected node paths. As long + as our __nps dictionary is kept up to date we shouldn't have to + iterate through the entire tree to find the items we need. + """ + self._selUpdating = True + + items = [self._nps[np] for np in msg.data if np in self._nps] + self.SelectItems( items ) + + self._selUpdating = False \ No newline at end of file diff --git a/src/pandaEditor/ui/viewport.py b/src/pandaEditor/ui/viewport.py new file mode 100644 index 0000000..74acb6c --- /dev/null +++ b/src/pandaEditor/ui/viewport.py @@ -0,0 +1,25 @@ +import os + +import wx + +import p3d +from wxExtra import CompositeDropTarget + + +class Viewport( p3d.wx.Viewport ): + + def __init__( self, *args, **kwargs ): + p3d.wx.Viewport.__init__( self, *args, **kwargs ) + + self.dt = CompositeDropTarget( ['filePath'], self.OnDropItem, self.ValidateDropItem ) + self.SetDropTarget( self.dt ) + + def OnDropItem( self, arg ): + + # Do the actual dropping next frame. This will allow the picker time + # to traverse the scene and find the node the mouse is over. + taskMgr.doMethodLater( 0, wx.GetApp().OnDragDrop, 'dragDrop', [arg] ) + + def ValidateDropItem( self, x, y ): + return True + \ No newline at end of file diff --git a/src/utils/__init__.py b/src/utils/__init__.py new file mode 100644 index 0000000..9e09b3f --- /dev/null +++ b/src/utils/__init__.py @@ -0,0 +1,5 @@ +from functions import * + +from wrappedFunction import WrappedFunction +from directoryWatcher import DirectoryWatcher +from singleton import Singleton \ No newline at end of file diff --git a/src/utils/directoryWatcher.py b/src/utils/directoryWatcher.py new file mode 100644 index 0000000..3d71846 --- /dev/null +++ b/src/utils/directoryWatcher.py @@ -0,0 +1,88 @@ +import os +import sys +import time +import threading + + +class DirectoryWatcher( threading.Thread ): + + """ + Class for watching a directory and all subdirectories below it for + changes. + """ + + def __init__( self, root=False ): + threading.Thread.__init__( self ) + + self.root = root + + self.daemon = True + self.running = False + + def _recurse( self, dirPath ): + """ + + """ + fDict = {} + + def setDict( key ): + fDict[key] = os.path.getmtime( key ) + + if self.root: + noval = [ + ([setDict( path + '\\' + f ) for f in files if files], + setDict( path )) + for path, dirs, files in os.walk( dirPath, True ) + ] + else: + noval = [ + ([setDict( path + '\\' + f ) for f in files if files], + [setDict( path + '\\' + f ) for f in dirs if dirs]) + for path, dirs, files in os.walk( dirPath, True ) + ] + + return fDict + + def setDirectory( self, dirPath ): + """Set the directory for watching.""" + self.dirPath = dirPath + self.before = self._recurse( self.dirPath ) + + def run( self ): + """ + Main watcher function. Don't use this to start the watcher, use + start() to run the daemon instead. + """ + self.running = True + while True: + after = self._recurse( self.dirPath ) + + # Work out which files were added, removed or modified + added = [f for f in after if not f in self.before] + removed = [f for f in self.before if not f in after] + modified = [ + f for f in after + if f in self.before and after[f] != self.before[f] + ] + + # Call handlers + if added: + self.onAdded( added ) + if removed: + self.onRemoved( removed ) + if modified: + self.onModified( modified ) + + self.before = after + + # Sleep a bit so we don't max out the thread + time.sleep( 1 ) + + def onAdded( self, filePaths ): + pass + + def onRemoved( self, filePaths ): + pass + + def onModified( self, filePaths ): + pass \ No newline at end of file diff --git a/src/utils/functions.py b/src/utils/functions.py new file mode 100644 index 0000000..73a8614 --- /dev/null +++ b/src/utils/functions.py @@ -0,0 +1,18 @@ +def Indent( elem, level=0, indent=' ' ): + """ + Function used to 'prettify' output xml from cElementTree's tree.getroot() + method into lines so it's easily read. + """ + i = "\n" + level * indent + if len( elem ): + if not elem.text or not elem.text.strip(): + elem.text = i + indent + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for elem in elem: + Indent( elem, level + 1, indent ) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + if level and ( not elem.tail or not elem.tail.strip() ): + elem.tail = i \ No newline at end of file diff --git a/src/utils/singleton.py b/src/utils/singleton.py new file mode 100644 index 0000000..fe53cda --- /dev/null +++ b/src/utils/singleton.py @@ -0,0 +1,14 @@ +class Singleton( object ): + + """Base singleton class.""" + + def __new__( cls, *args, **kwargs ): + if not '_the_instance' in cls.__dict__: + cls._the_instance = object.__new__( cls ) + return cls._the_instance + + @classmethod + def _reset( cls ): + """Reset the borg""" + if '_the_instance' in cls.__dict__: + del cls._the_instance \ No newline at end of file diff --git a/src/utils/wrappedFunction.py b/src/utils/wrappedFunction.py new file mode 100644 index 0000000..1231c2e --- /dev/null +++ b/src/utils/wrappedFunction.py @@ -0,0 +1,30 @@ +class WrappedFunction( object ): + + def __init__( self, fn=None ): + self.fns = [] + + # Add base function + if fn is not None: + self.Add( fn ) + + def __call__( self, *args, **kwargs ): + return self.fn( *args, **kwargs ) + + def Add( self, fn ): + """Add a function.""" + self.fns.append( fn ) + self.Compile() + + def Compile( self ): + """Compile the final function.""" + compiledFn = self.fns[0] + for i in range( 1, len( self.fns ) ): + compiledFn = self.Wrap( compiledFn, self.fns[i] ) + self.fn = compiledFn + + def Wrap( self, fn, wrapFn ): + """Return function fn wrapped with wrapFn.""" + def Wrapped( *args ): + return wrapFn( *fn( *args ) ) + return Wrapped + \ No newline at end of file diff --git a/src/wxExtra/__init__.py b/src/wxExtra/__init__.py new file mode 100644 index 0000000..39eb9ab --- /dev/null +++ b/src/wxExtra/__init__.py @@ -0,0 +1,10 @@ +import utils +from logPanel import LogPanel +from dirTreeCtrl import DirTreeCtrl +from auiManagerConfig import AuiManagerConfig +from customMenu import CustomMenu +from customTreeCtrl import CustomTreeCtrl +import propertyGrid as wxpg +from actionItem import ActionItem +from customAuiToolBar import CustomAuiToolBar +from compositeDropTarget import CompositeDropTarget \ No newline at end of file diff --git a/src/wxExtra/actionItem.py b/src/wxExtra/actionItem.py new file mode 100644 index 0000000..beec89d --- /dev/null +++ b/src/wxExtra/actionItem.py @@ -0,0 +1,36 @@ +import wx + + +class ActionItem( object ): + + def __init__( self, text, iconPath, cmd, id=None, args=[], check=False ): + self._text = text + self._iconPath = iconPath + self._cmd = cmd + self._id = id + self._args = args + self._check = check + + # Generate a unique id if one wasn't supplied + if self._id is None: + self._id = wx.NewId() + + def GetText( self ): + return self._text + + def GetIconPath( self ): + return self._iconPath + + def GetCommand( self ): + return self._cmd + + def GetId( self ): + return self._id + + def GetArguments( self ): + return self._args + + def GetCheck( self ): + return self._check + + \ No newline at end of file diff --git a/src/wxExtra/auiManagerConfig.py b/src/wxExtra/auiManagerConfig.py new file mode 100644 index 0000000..6f666b4 --- /dev/null +++ b/src/wxExtra/auiManagerConfig.py @@ -0,0 +1,85 @@ +import wx + + +class AuiManagerConfig( wx.Config ): + + """ + Custom wxConfig class to handle the main frame size and position, plus all + the the panes of the aui. + """ + + def __init__( self, auiMgr, *args, **kwargs ): + wx.Config.__init__( self, *args, **kwargs ) + + self.auiMgr = auiMgr + self.win = self.auiMgr.GetManagedWindow() + + # Key constants + if self.win is not None: + winName = self.win.GetName() + self._keyWinPosX = winName + 'PosX' + self._keyWinPosY = winName + 'PosY' + self._keyWinSizeX = winName + 'SizeX' + self._keyWinSizeY = winName + 'SizeY' + self._keyWinMax = winName + 'Max' + self._keyPerspDefault = 'perspDefault' + + def Save( self ): + """Save all panel layouts for the aui manager.""" + # Get old window position and size. We'll use these instead of the + # maximized window's size and position. + winPosX = self.ReadInt( self._keyWinPosX ) + winPosY = self.ReadInt( self._keyWinPosY ) + winSizeX = self.ReadInt( self._keyWinSizeX ) + winSizeY = self.ReadInt( self._keyWinSizeY ) + + self.DeleteAll() + + if self.win is not None: + + # Don't save maximized window properties + if not self.win.IsMaximized(): + winPosX, winPosY = self.win.GetPosition() + winSizeX, winSizeY = self.win.GetSize() + + # Save the managed window position and size + self.SavePosition( winPosX, winPosY ) + self.SaveSize( winSizeX, winSizeY ) + + # Save the managed window state + winMax = self.win.IsMaximized() + self.WriteBool( self._keyWinMax, winMax ) + + # Save the current perspective as the default + self.Write( self._keyPerspDefault, self.auiMgr.SavePerspective() ) + + def SavePosition( self, x, y ): + """Save the managed window's position.""" + self.WriteInt( self._keyWinPosX, x ) + self.WriteInt( self._keyWinPosY, y ) + + def SaveSize( self, x, y ): + """Save the managed window's size.""" + self.WriteInt( self._keyWinSizeX, x ) + self.WriteInt( self._keyWinSizeY, y ) + + def Load( self ): + """Load all panel layouts for the aui manager.""" + if self.win is not None: + + # Load the managed window state + winMax = self.ReadBool( self._keyWinMax ) + self.win.Maximize( winMax ) + + # Load the managed window size + winSizeX = self.ReadInt( self._keyWinSizeX ) + winSizeY = self.ReadInt( self._keyWinSizeY ) + self.win.SetSize( (winSizeX, winSizeY) ) + + # Load the managed window position + winPosX = self.ReadInt( self._keyWinPosX ) + winPosY = self.ReadInt( self._keyWinPosY ) + self.win.SetPosition( (winPosX, winPosY) ) + + # Load the default perspective + self.auiMgr.LoadPerspective( self.Read( self._keyPerspDefault ) ) \ No newline at end of file diff --git a/src/wxExtra/compositeDropTarget.py b/src/wxExtra/compositeDropTarget.py new file mode 100644 index 0000000..bfab8ac --- /dev/null +++ b/src/wxExtra/compositeDropTarget.py @@ -0,0 +1,43 @@ +import wx + + +class CompositeDropTarget( wx.PyDropTarget ): + + def __init__( self, formatNames, fn, validateFn ): + wx.PyDropTarget.__init__( self ) + + self.fn = fn + self.validateFn = validateFn + + # Specify the type of data we will accept + self.doc = wx.DataObjectComposite() + self.formats = {} + for formatName in formatNames: + do = wx.CustomDataObject( formatName ) + self.formats[formatName] = do + self.doc.Add( do ) + self.SetDataObject( self.doc ) + + def OnDragOver( self, x, y, d ): + + # Return x.DragNone if the validation fails + if not self.validateFn( x, y ): + return wx.DragNone + else: + return d + + def OnData( self, x, y, d ): + + # Save mouse drop coords + self.x = x + self.y = y + + # Copy the data from the drag source to our data object + if self.GetData(): + + # Call the method on the string taken from the data object + format = self.doc.GetReceivedFormat().GetId() + do = self.formats[format] + self.fn( do.GetData() ) + + return d \ No newline at end of file diff --git a/src/wxExtra/customAuiToolBar.py b/src/wxExtra/customAuiToolBar.py new file mode 100644 index 0000000..b695632 --- /dev/null +++ b/src/wxExtra/customAuiToolBar.py @@ -0,0 +1,46 @@ +import wx + +import utils + + +class CustomAuiToolBar( wx.aui.AuiToolBar ): + + def __init__( self, *args, **kwargs ): + wx.aui.AuiToolBar.__init__( self, *args, **kwargs ) + self._bmpSize = None + + def AppendActionItem( self, actn ): + actnIcon = utils.ImgToBmp( actn.GetIconPath(), self.GetToolBitmapSize() ) + self.AddTool( actn.GetId(), actn.GetText(), actnIcon ) + self.Bind( wx.EVT_TOOL, actn.GetCommand(), id=actn.GetId() ) + + def AppendActionItems( self, actns ): + for actn in actns: + self.AppendActionItem( actn ) + + def GetToolBitmapSize( self ): + """ + Workaround as GetToolBitmapSize seems only to return the default + icon size. + """ + if self._bmpSize is None: + return wx.aui.AuiToolBar.GetToolBitmapSize( self ) + + return self._bmpSize + + def SetToolBitmapSize( self, size ): + """ + Workaround as GetToolBitmapSize seems only to return the default + icon size. + """ + wx.aui.AuiToolBar.SetToolBitmapSize( self, size ) + + self._bmpSize = size + + def EnableAllTools( self, state ): + """Enable or disable all tools in the toolbar.""" + for i in range( self.GetToolCount() ): + tool = self.FindToolByIndex( i ) + self.EnableTool( tool.GetId(), state ) + self.Refresh() + \ No newline at end of file diff --git a/src/wxExtra/customMenu.py b/src/wxExtra/customMenu.py new file mode 100644 index 0000000..8da0fa6 --- /dev/null +++ b/src/wxExtra/customMenu.py @@ -0,0 +1,58 @@ +import wx + +import utils + + +class CustomMenu( wx.Menu ): + + """ + Custom wxMenu class with convenience methods to add menu items with + icons. + """ + + def AppendActionItem( self, actn, parent ): + actnText = actn.GetText() + if not actnText.startswith( '&' ): + actnText = '&' + actnText + + mItem = wx.MenuItem( self, actn.GetId(), actnText ) + + # Create the icon if iconPath is present + iconPath = actn.GetIconPath() + if False: # Disabled for now + img = wx.Image( iconPath, wx.BITMAP_TYPE_ANY ) + img.Rescale( 16, 16, quality=wx.IMAGE_QUALITY_HIGH ) + mItem.SetBitmap( img.ConvertToBitmap() ) + + # Append check item or regular item + if actn.GetCheck(): + self.AppendCheckItem( actn.GetId(), actnText ) + else: + self.AppendItem( mItem ) + + # Bind the menu event - use args if provided. + args = actn.GetArguments() + if args: + utils.IdBind( parent, wx.EVT_MENU, actn.GetId(), actn.GetCommand(), args ) + else: + parent.Bind( wx.EVT_MENU, actn.GetCommand(), id=actn.GetId() ) + + def AppendActionItems( self, actns, parent ): + for actn in actns: + self.AppendActionItem( actn, parent ) + + def AppendIconItem( self, id, text, help, iconPath ): + + # Create the icon and resize + img = wx.Image( iconPath, wx.BITMAP_TYPE_ANY ) + img.Rescale( 16, 16 ) + + # Create and append the new menu item + mItem = wx.MenuItem( self, id, text, help ) + mItem.SetBitmap( img.ConvertToBitmap() ) + self.AppendItem( mItem ) + + def EnableAllTools( self, state ): + """Enable or disable all tools in the toolbar.""" + for item in self.GetMenuItems(): + item.Enable( state ) \ No newline at end of file diff --git a/src/wxExtra/customTreeCtrl.py b/src/wxExtra/customTreeCtrl.py new file mode 100644 index 0000000..f47f0e8 --- /dev/null +++ b/src/wxExtra/customTreeCtrl.py @@ -0,0 +1,60 @@ +import wx +import wx.lib.agw.customtreectrl as ct + + +class CustomTreeCtrl( ct.CustomTreeCtrl ): + + def __init__( self, *args, **kwargs ): + ct.CustomTreeCtrl.__init__( self, *args, **kwargs ) + + self.SetBorderPen( wx.Pen( (0, 0, 0), 0, wx.TRANSPARENT ) ) + self.EnableSelectionGradient( True ) + self.SetGradientStyle( True ) + self.SetFirstGradientColour( wx.Color(46, 46, 46) ) + self.SetSecondGradientColour( wx.Color(123, 123, 123) ) + + def GetItemChildren( self, parentItem ): + + """ + wxPython's standard tree control does not have a get item children + method by default. + """ + + children = [] + + item, cookie = self.GetFirstChild( parentItem ) + + while item is not None and item.IsOk(): + children.append( item ) + item = self.GetNextSibling( item ) + + return children + + def FindItemByText( self, text ): + + """ + Iterate through all items and return the first which matches the given + text. + """ + + def __Recurse( item, text ): + for child in self.GetItemChildren( item ): + if self.GetItemText( child ) == text: + return child + + __Recurse( child, text ) + + return __Recurse( self.GetRootItem(), text ) + + def GetAllItems( self ): + + """Return a list of all items in the control.""" + + def __GetChildren( item, allItems ): + for child in self.GetItemChildren( item ): + allItems.append( child ) + __GetChildren( child, allItems ) + + allItems = [] + __GetChildren( self.GetRootItem(), allItems ) + return allItems \ No newline at end of file diff --git a/src/wxExtra/dirTreeCtrl.py b/src/wxExtra/dirTreeCtrl.py new file mode 100644 index 0000000..a602f38 --- /dev/null +++ b/src/wxExtra/dirTreeCtrl.py @@ -0,0 +1,314 @@ +""" + dirTreeCtrl + + @summary: A tree control for use in displaying directories + @author: Collin Green aka Keeyai + @url: http://keeyai.com + @license: public domain -- use it how you will, but a link back would be nice + @version: 0.9.0 + @note: + behaves just like a TreeCtrl + + Usage: + set your default and directory images using addIcon -- see the commented + last two lines of __init__ + + initialze the tree then call SetRootDir(directory) with the root + directory you want the tree to use + + use SetDeleteOnCollapse(bool) to make the tree delete a node's children + when the node is collapsed. Will (probably) save memory at the cost of + a bit o' speed + + use addIcon to use your own icons for the given file extensions + + + @todo: + extract ico from exes found in directory +""" + +import os + +import wx + + +ICON_SIZE = (16, 16) + + +class Directory: + + """Simple class for using as the data object in the DirTreeCtrl.""" + + __name__ = 'Directory' + def __init__( self, directory='' ): + self.directory = directory + + +class DirTreeCtrl( wx.TreeCtrl ): + + """ + A wx.TreeCtrl that is used for displaying directory structures. Virtually + handles paths to help with memory management. + """ + + def __init__( self, parent, *args, **kwds ): + """ + Initializes the tree and binds some events we need for making this + dynamically load its data. + """ + wx.TreeCtrl.__init__( self, parent, *args, **kwds ) + + # Bind events + self.Bind( wx.EVT_TREE_ITEM_EXPANDING, self.TreeItemExpanding ) + self.Bind( wx.EVT_TREE_ITEM_COLLAPSING, self.TreeItemCollapsing ) + + # Option to delete node items from tree when node is collapsed + self.DELETEONCOLLAPSE = False + + # Some hack-ish code here to deal with imagelists + self.iconentries = {} + self.imagelist = wx.ImageList( *ICON_SIZE ) + + # Set default images + self.iconentries['default'] = self.imagelist.Add( wx.ArtProvider.GetBitmap( wx.ART_NORMAL_FILE, wx.ART_OTHER, ICON_SIZE ) ) + self.iconentries['directory'] = self.imagelist.Add( wx.ArtProvider.GetBitmap( wx.ART_FOLDER, wx.ART_OTHER, ICON_SIZE ) ) + + def addIcon( self, filepath, wxBitmapType, name ): + """ + Adds an icon to the imagelist and registers it with the iconentries + dict using the given name. Use so that you can assign custom icons to + the tree just by passing in the value stored in self.iconentries[name] + + @param filepath: + path to the image + @param wxBitmapType: + wx constant for the file type - eg wx.BITMAP_TYPE_PNG + @param name: + name to use as a key in the self.iconentries dict - get your + imagekey by calling self.iconentries[name] + """ + try: + if os.path.exists( filepath ): + key = self.imagelist.Add( wx.Bitmap( filepath, wxBitmapType ) ) + self.iconentries[name] = key + except Exception, e: + print e + + def SetDeleteOnCollapse( self, selection ): + """ + Sets the tree option to delete leaf items when the node is collapsed. + Will slow down the tree slightly but will probably save memory. + """ + if type( selection ) == type( True ): + self.DELETEONCOLLAPSE = selection + + def SetRootDir( self, directory ): + """ + Sets the root directory for the tree. Throws an exception if the + directory is invalid. + + @param directory: + directory to load + """ + + # Check if directory exists and is a directory + if not os.path.isdir(directory): + raise Exception("%s is not a valid directory" % directory) + + # Delete existing root, if any + self.DeleteAllItems() + + # Add directory as root + root = self.AddRoot( directory ) + self.SetPyData( root, Directory( directory ) ) + self.SetItemImage( root, self.iconentries['directory'] ) + self.Expand( root ) + + # Load items + self._loadDir( root, directory ) + + def _loadDir( self, item, directory ): + """ + Private function that gets called to load the file list for the given + directory and append the items to the tree. Throws an exception if the + directory is invalid. + + @note: + does not add items if the node already has children + """ + # check if directory exists and is a directory + if not os.path.isdir( directory ): + raise Exception( "%s is not a valid directory" % directory ) + + # NOTE: Changed this to completely rebulid whenever a directory is + # expanded. + + # Delete all children + self.DeleteChildren( item ) + + # Get files in directory + files = os.listdir( directory ) + + # Add nodes to tree + for f in files: + + # Process the file extension to build image list + imagekey = self.processFileExtension(os.path.join(directory, f)) + + # If directory, tell tree it has children + if os.path.isdir( os.path.join( directory, f ) ): + + # Add item to + child = self.AppendItem(item, f, image=imagekey) + self.SetItemHasChildren(child, True) + + # Save item path for expanding later + self.SetPyData( child, Directory( os.path.join( directory, f ) ) ) + + else: + self.AppendItem( item, f, image=imagekey ) + + def getFileExtension( self, filename ): + """Helper function for getting a file's extension""" + # check if directory + if not os.path.isdir(filename): + + # search for the last period + index = filename.rfind( '.' ) + if index > -1: + return filename[index:] + return '' + else: + return 'directory' + + def processFileExtension( self, filename ): + """ + Helper function. Called for files and collects all the necessary icons + into in image list which is re-passed into the tree every time + (imagelists are a lame way to handle images) + """ + ext = self.getFileExtension(filename) + ext = ext.lower() + + excluded = ['', '.exe', '.ico'] + # do nothing if no extension found or in excluded list + if ext not in excluded: + + # only add if we dont already have an entry for this item + if ext not in self.iconentries.keys(): + + # sometimes it just crashes + try: + # use mimemanager to get filetype and icon + # lookup extension + filetype = wx.TheMimeTypesManager.GetFileTypeFromExtension( ext ) + + if hasattr(filetype, 'GetIconInfo'): + info = filetype.GetIconInfo() + + if info is not None: + icon = info[0] + if icon.Ok(): + # add to imagelist and store returned key + iconkey = self.imagelist.AddIcon(icon) + self.iconentries[ext] = iconkey + + # update tree with new imagelist - inefficient + self.SetImageList(self.imagelist) + + # return new key + return iconkey + except: + return self.iconentries['default'] + + # already have icon, return key + else: + return self.iconentries[ext] + + # if exe, get first icon out of it + elif ext == '.exe': + #TODO: get icon out of exe withOUT using weird winpy BS + pass + + # if ico just use it + elif ext == '.ico': + try: + icon = wx.Icon(filename, wx.BITMAP_TYPE_ICO) + if icon.IsOk(): + return self.imagelist.AddIcon(icon) + + except Exception, e: + print e + return self.iconentries['default'] + + # if no key returned already, return default + return self.iconentries['default'] + + def TreeItemExpanding( self, event ): + """ + Called when a node is about to expand. Loads the node's files from the + file system. + """ + item = event.GetItem() + + # check if item has directory data + if type(self.GetPyData(item)) == type(Directory()): + d = self.GetPyData(item) + self._loadDir(item, d.directory) + else: + # print 'no data found!' + pass + + event.Skip() + + def TreeItemCollapsing( self, event ): + """ + Called when a node is about to collapse. Removes the children from the + tree if self.DELETEONCOLLAPSE is set - see L{SetDeleteOnCollapse} + """ + item = event.GetItem() + + # delete the node's children if that tree option is set + if self.DELETEONCOLLAPSE: + self.DeleteChildren(item) + + event.Skip() + + def GetItemPath( self, itemId ): + """Return a full path for the indicated item id.""" + # If PyData is set then the item is a directory so send that path back + dir = self.GetPyData( itemId ) + if dir is not None: + return dir.directory + + # Otherwise we have to create the path from the item's name and the + # parent item's directory path + pItemId = self.GetItemParent( itemId ) + dirPath = self.GetPyData( pItemId ).directory + return os.path.join( dirPath, self.GetItemText( itemId ) ) + + def GetAllItems( self ): + """Return a list of all items in the control.""" + def GetChildren( item, allItems ): + for child in self.GetItemChildren( item ): + allItems.append( child ) + GetChildren( child, allItems ) + + allItems = [] + GetChildren( self.GetRootItem(), allItems ) + return allItems + + def GetItemChildren( self, parentItem ): + """ + wxPython's standard tree control does not have a get item children + method by default. + """ + children = [] + + item, cookie = self.GetFirstChild( parentItem ) + + while item is not None and item.IsOk(): + children.append( item ) + item = self.GetNextSibling( item ) + + return children \ No newline at end of file diff --git a/src/wxExtra/logPanel.py b/src/wxExtra/logPanel.py new file mode 100644 index 0000000..bbcb6b6 --- /dev/null +++ b/src/wxExtra/logPanel.py @@ -0,0 +1,58 @@ +import sys +import datetime + +import wx + + +class LogPanel( wx.Panel ): + + """ + Simple wxPanel containing a text control which will display the stdout and + stderr streams. + """ + + class RedirectText( object ): + + def __init__( self, terminal, textCtrl ): + self.terminal = terminal + self.textCtrl = textCtrl + + # Set err to True if the stream is stderr + self.err = False + if terminal is sys.stderr: + self.err = True + + def write( self, text ): + self.terminal.write( text ) + self.textCtrl.WriteText( text ) + + # If the text came from stderr, thaw the top window of the + # application or else we won't see the message! + if self.err: + wx.CallAfter( self.ThawTopWindow ) + + def ThawTopWindow( self ): + """ + If the application has thrown an assertion while the top frame has + been frozen then we won't be able to see the text. This method once + called after the write() method above will make sure the top frame + is thawed - making the text visible. + """ + topWin = wx.GetApp().GetTopWindow() + if topWin.IsFrozen(): + topWin.Thaw() + + def __init__( self, *args, **kwargs ): + wx.Panel.__init__( self, *args, **kwargs ) + + # Build log text control + self.tc = wx.TextCtrl( self, style=wx.TE_MULTILINE | wx.TE_RICH2 ) + + # Redirect text here + sys.stdout = self.RedirectText( sys.stdout, self.tc ) + sys.stderr = self.RedirectText( sys.stderr, self.tc ) + + # Build sizers + self.bs1 = wx.BoxSizer( wx.VERTICAL ) + self.bs1.Add( self.tc, 1, wx.EXPAND ) + self.SetSizer( self.bs1 ) \ No newline at end of file diff --git a/src/wxExtra/propertyGrid.py b/src/wxExtra/propertyGrid.py new file mode 100644 index 0000000..8e8c08c --- /dev/null +++ b/src/wxExtra/propertyGrid.py @@ -0,0 +1,423 @@ +import wx +import wx.lib.intctrl +import wx.lib.agw.floatspin as fs +from wx.lib.embeddedimage import PyEmbeddedImage +from wx.lib.newevent import NewEvent + + +expand = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAAZiS0dE" + "AP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9oCBRUpC/sWEUMAAAA9" + "SURBVDjLY2AY1uA/MYqYKDWEiVKXMFHqHSZKw4SJ0oAl1QBGSgxgpMQLjCQHIiMTBOPTjN9K" + "JuJS4sADAOgbBxlBsfXrAAAAAElFTkSuQmCC" + ) + +collapse = PyEmbeddedImage( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAAZiS0dE" + "AP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9oCBRMnAPbKmfcAAABB" + "SURBVDjLY2AYBcMAMKJwmBj+//9HhCYmBob//yB6GbFI4jUEWTNWA6DgP9GuxqPwPyHNhAxA" + "N4SR3ID+P8wTEgBwLg4FdgEHxgAAAABJRU5ErkJggg==" + ) + + +EvtIconClick, EVT_ICON_CLICK = NewEvent() +EvtIconToggle, EVT_ICON_TOGGLE = NewEvent() + +wxEVT_PG_CHANGED = wx.NewEventType() +EVT_PG_CHANGED = wx.PyEventBinder( wxEVT_PG_CHANGED, 0 ) + +wxEVT_PG_RIGHT_CLICK = wx.NewEventType() +EVT_PG_RIGHT_CLICK = wx.PyEventBinder( wxEVT_PG_RIGHT_CLICK, 0 ) + + +class PropertyGridEvent( wx.PyCommandEvent ): + + def __init__( self, evtType ): + wx.PyCommandEvent.__init__( self, evtType ) + + self._prop = None + + def GetProperty( self ): + return self._prop + + def SetProperty( self, prop ): + self._prop = prop + + +class BaseProperty( object ): + + def __init__( self, label, name, value ): + self._label = label + self._name = name + self._value = value + + self._window = None + self._grid = None + self._parent = None + self._children = [] + self._attrs = {} + + def GetLabel( self ): + return self._label + + def GetName( self ): + return self._name + + def GetValue( self ): + return self._value + + def SetValue( self, value ): + self._value = value + + def GetGrid( self ): + return self._grid + + def SetGrid( self, grid ): + self._grid = grid + + def GetParent( self ): + return self._parent + + def SetParent( self, parent ): + self._parent = parent + + def GetCount( self ): + return len( self._children ) + + def Item( self, index ): + return self._children[index] + + def GetChildren( self ): + return self._children + + def AddPrivateChild( self, child ): + self._children.append( child ) + + def GetAttribute( self, name ): + return self._attrs[name] + + def SetAttribute( self, name, value ): + self._attrs[name] = value + + def IsCategory( self ): + return False + + def IsExpanded( self ): + return self._window.IsExpanded() + + def SetExpanded( self, val ): + self._window.Expand( val ) + + def SetWindow( self, win ): + self._window = win + + def BuildControl( self, *args, **kwargs ): + return None + + def SetValueFromEvent( self, evt ): + ctrl = evt.GetEventObject() + self.SetValue( ctrl.GetValue() ) + + def OnChanged( self, evt ): + self.SetValueFromEvent( evt ) + #event = PropertyGridEvent( wxEVT_PG_CHANGED ) + #event.SetProperty( self ) + #evt.GetEventObject().GetEventHandler().ProcessEvent( event ) + + # Call after otherwise we crash! + #fn = lambda evt: evt.GetEventObject().GetEventHandler().ProcessEvent( event ) + #wx.CallAfter( fn ) + + #fn = lambda event: self.GetGrid().GetEventHandler().ProcessEvent( event ) + #wx.CallAfter( fn ) + + # Call after otherwise we crash! + evt = PropertyGridEvent( wxEVT_PG_CHANGED ) + evt.SetProperty( self ) + fn = lambda : self.GetGrid().GetEventHandler().ProcessEvent( evt ) + wx.CallAfter( fn ) + + +class PropertyCategory( BaseProperty ): + + def __init__( self, label, **kwargs ): + BaseProperty.__init__( self, label, '', None, **kwargs ) + + def IsCategory( self ): + return True + + +class BoolProperty( BaseProperty ): + + def __init__( self, *args, **kwargs ): + BaseProperty.__init__( self, *args, **kwargs ) + + def SetValueFromEvent( self, evt ): + self._value = evt.IsChecked() + + def BuildControl( self, parent, id ): + ctrl = wx.CheckBox( parent, id, style=wx.ALIGN_RIGHT ) + ctrl.SetValue( self.GetValue() ) + ctrl.Bind( wx.EVT_CHECKBOX, self.OnChanged ) + return ctrl + + +class IntProperty( BaseProperty ): + + def __init__( self, *args, **kwargs ): + BaseProperty.__init__( self, *args, **kwargs ) + + def BuildControl( self, parent, id ): + ctrl = wx.lib.intctrl.IntCtrl( parent, id ) + ctrl.SetValue( self.GetValue() ) + ctrl.Bind( wx.lib.intctrl.EVT_INT, self.OnChanged ) + return ctrl + + +class FloatProperty( BaseProperty ): + + def __init__( self, *args, **kwargs ): + BaseProperty.__init__( self, *args, **kwargs ) + + def BuildControl( self, parent, id ): + ctrl = fs.FloatSpin( parent, id, value=self.GetValue() ) + ctrl.Bind( fs.EVT_FLOATSPIN, self.OnChanged ) + return ctrl + + +class StringProperty( BaseProperty ): + + def __init__( self, *args, **kwargs ): + BaseProperty.__init__( self, *args, **kwargs ) + + def BuildControl( self, parent, id ): + ctrl = wx.TextCtrl( parent, id, value=str( self.GetValue() ) ) + ctrl.Bind( wx.EVT_TEXT, self.OnChanged ) + return ctrl + + +class ToggleIcon( wx.Panel ): + def __init__( self, parent, id, bitmapTrue, bitmapFalse, pos=wx.DefaultPosition ): + if bitmapTrue.GetSize() !=bitmapFalse.GetSize(): + raise Exception( 'Bitmaps are different sizes!' ) + size = bitmapTrue.GetSize() + + wx.Panel.__init__( self, parent, id, pos, size ) + + self._bitmapTrue = bitmapTrue + self._bitmapFalse = bitmapFalse + self._disabled_bitmap = True + + self._hover = False + self._state = False + + #self.SetBackgroundColour( self.Parent.GetBackgroundColour() ) + + self.Bind( wx.EVT_PAINT, self.OnPaint ) + #self.Bind( wx.EVT_SIZE, self.OnSize ) + self.Bind( wx.EVT_LEFT_DOWN, self.OnButton ) + + def SetState( self, flag=True ): + """Set the state of the toogle.""" + self._state = bool( flag ) + self.Refresh() + + def GetState( self ): + """Get the state of the toogle.""" + return self._state + + def Toggle( self ): + """Switch state""" + self._state = not self._state + self.Refresh() + + def OnPaint( self, event ): + dc = wx.PaintDC( self ) + if self.IsEnabled(): + if self._state is True: + dc.DrawBitmap( self._bitmapTrue, 0, 0, True ) + else: + dc.DrawBitmap( self._bitmapFalse, 0, 0, True ) + elif self._disabled_bitmap is not False: + if self._state is True: + dc.DrawBitmap( self._bitmapTrue, 0, 0, True ) + else: + dc.DrawBitmap( self._bitmapFalse, 0, 0, True ) + + #def OnSize(self, event): + # self.Refresh() + + def OnButton( self, evt ): + if self.IsEnabled(): + self.Toggle() + evt = EvtIconToggle( state=self._state ) + self.ProcessEvent( evt ) + + +class BasePanel( wx.Panel ): + + def __init__( self, grid, prop, *args, **kwargs ): + wx.Panel.__init__( self, *args, **kwargs ) + + self.grid = grid + self.prop = prop + self._windows = [] + self._margin = 0 + + # Build sizers + self.sizer = wx.BoxSizer( wx.VERTICAL ) + self.SetSizer( self.sizer ) + + def AddWindow( self, window ): + self.sizer.Add( window, 0, wx.LEFT | wx.EXPAND, self._margin ) + self._windows.append( window ) + + def GetWindows( self ): + return self._windows + + def ClearWindows( self ): + for win in self._windows: + win.Destroy() + self._windows = [] + + +class CollapsiblePanel( BasePanel ): + + def __init__( self, *args, **kwargs ): + BasePanel.__init__( self, *args, **kwargs ) + + self.SetBackgroundColour( wx.Colour( 255, 255, 255 ) ) + + self.hidden = False + self._margin = 5 + + # Add the label + self.label = wx.StaticText( self, -1, self.prop.GetLabel() ) + if self.prop.IsCategory(): + labelfont = wx.SystemSettings_GetFont( wx.SYS_DEFAULT_GUI_FONT ) + labelfont.SetWeight( wx.BOLD ) + self.label.SetFont( labelfont ) + + # Button and label go in seperate sizer + self.sizer2 = wx.BoxSizer( wx.HORIZONTAL ) + self.sizer2.PrependSpacer( 16, -1 ) + self.sizer2.Add( self.label, 1, wx.EXPAND ) + self.sizer.Add( self.sizer2, 1, wx.EXPAND ) + + # Create the control + ctrl = self.prop.BuildControl( self, -1 ) + if ctrl is not None: + self.sizer2.Add( ctrl, 2, wx.RIGHT, 5 ) + + self.Collapse() + + def BuildButton( self ): + self.but = ToggleIcon( self, -1, collapse.GetBitmap(), expand.GetBitmap() ) + self.but.SetBackgroundColour( self.GetBackgroundColour() ) + #self.but.SetState( not self.hidden ) + self.but.Bind( EVT_ICON_TOGGLE, self.OnButton ) + self.sizer2.Prepend( self.but, 0, wx.ALIGN_CENTER, 0 ) + + # Remove the spacer now that there is a button + self.sizer2.Remove( 0 ) + + def AddWindow( self, window ): + BasePanel.AddWindow( self, window ) + window.Show( not self.hidden ) + + # Add collapse button + if not hasattr( self, 'but' ): + self.BuildButton() + + def SetBackgroundColour(self,colour): + if hasattr( self, 'but' ): + self.but.SetBackgroundColour( colour ) + wx.Panel.SetBackgroundColour( self, colour ) + + def SetLabelFont( self, font ): + """Set the label font.""" + self.label.SetFont( font ) + + def Expand( self, flag=True ): + """Expand (or collapse) the panel, flag=True to expand""" + for win in self._windows: + win.Show( flag ) + self.hidden = not flag + + # Make sure to update the button to reflect the current state + if hasattr( self, 'but' ): + self.but.SetState( not self.hidden ) + + # Layout has changed, make sure to refresh the entire tree. + self.grid.RecurseLayout( self.grid.panel ) + + def Collapse( self ): + """Collapse the panel.""" + self.Expand( False ) + + def IsExpanded( self ): + return not self.hidden + + def OnButton( self, evt ): + self.Expand( self.hidden ) + + +class PropertyGrid( wx.Panel ): + + def __init__( self, *args, **kwargs ): + wx.Panel. __init__( self, *args, **kwargs ) + self.SetBackgroundColour( wx.Colour( 255, 255, 255 ) ) + + self._props = [] + self.panel = BasePanel( self, BaseProperty( '', '', None ), self ) + self._currParent = self.panel + + # Build sizer + self.sizer = wx.BoxSizer( wx.VERTICAL ) + self.sizer.Add( self.panel, 1, wx.EXPAND ) + self.SetSizer( self.sizer ) + + def Append( self, prop ): + + # Only parent categories to the top level. + if type( prop ) == PropertyCategory: + self._currParent = self.panel + + pnl = CollapsiblePanel( self, prop, self._currParent ) + self._currParent.AddWindow( pnl ) + prop.SetWindow( pnl ) + prop.SetGrid( self ) + + # HAXXOR - Need to get this to recurse. + # Add any children + for child in prop.GetChildren(): + cPnl = CollapsiblePanel( self, child, pnl ) + pnl.AddWindow( cPnl ) + child.SetWindow( cPnl ) + child.SetGrid( self ) + + for gChild in child.GetChildren(): + gcPnl = CollapsiblePanel( self, gChild, cPnl ) + cPnl.AddWindow( gcPnl ) + gChild.SetWindow( gcPnl ) + gChild.SetGrid( self ) + + prop.SetParent( self._currParent.prop ) + self._props.append( prop ) + + # Set current parent if the property was a category + if type( prop ) == PropertyCategory: + self._currParent = pnl + + def RecurseLayout( self, ctrl=None ): + """ + Recurse down the hierarchy calling Layout and Refresh on each window + to make sure it's the right size. + """ + ctrl.Layout() + ctrl.Refresh() + for child in ctrl.GetChildren(): + self.RecurseLayout( child ) + + def Clear( self ): + self.panel.DestroyChildren() \ No newline at end of file diff --git a/src/wxExtra/utils.py b/src/wxExtra/utils.py new file mode 100644 index 0000000..38b4a0c --- /dev/null +++ b/src/wxExtra/utils.py @@ -0,0 +1,95 @@ +import os + +import wx + + +def FileDialog( message, wildcard, style, defaultDir=os.getcwd(), defaultFile='' ): + """ + Generic file dialog method. If False is returned then the user has hit + cancel or not selected a valid path. + """ + dlg = wx.FileDialog( wx.GetApp().GetTopWindow(), message, defaultDir, defaultFile, wildcard, style ) + if dlg.ShowModal() == wx.ID_OK: + if style & wx.MULTIPLE: + result = dlg.GetPaths() + else: + result = dlg.GetPath() + else: + result = False + dlg.Destroy() + + return result + + +def FileOpenDialog( message, wildcard, style=0, defaultDir=os.getcwd(), defaultFile='' ): + """Generic file open dialog.""" + style = style | wx.OPEN | wx.CHANGE_DIR + return FileDialog( message, wildcard, style, defaultDir, defaultFile ) + + +def FileSaveDialog( message, wildcard, style=0, defaultDir=os.getcwd(), defaultFile='' ): + """Generic file save dialog.""" + style = style | wx.SAVE | wx.CHANGE_DIR + return FileDialog( message, wildcard, style, defaultDir, defaultFile ) + + +def DirDialog( message, defaultPath=os.getcwd(), style=wx.DD_DEFAULT_STYLE ): + """Generic directory dialog.""" + dlg = wx.DirDialog( wx.GetApp().GetTopWindow(), message, defaultPath, style ) + if dlg.ShowModal() == wx.ID_OK: + result = dlg.GetPath() + else: + result = False + dlg.Destroy() + + return result + + +def MessageDialog( message, caption, style ): + """Generic message dialog method.""" + dlg = wx.MessageDialog( wx.GetApp().GetTopWindow(), message, caption, style ) + result = dlg.ShowModal() + dlg.Destroy() + + return result + + +def InformationDialog( message, caption='Information' ): + """Generic information dialog with ok button.""" + return MessageDialog( message, caption, wx.ICON_INFORMATION | wx.OK ) + + +def WarningDialog( message, caption='Warning' ): + """Generic warning dialog with ok button.""" + return MessageDialog( message, caption, wx.ICON_WARNING | wx.OK ) + + +def ErrorDialog( message, caption='Error' ): + """Generic error dialog with ok button.""" + return MessageDialog( message, caption, wx.ICON_ERROR | wx.OK ) + + +def YesNoDialog( message, caption, style=wx.ICON_QUESTION ): + """Generic message dialog with yes / no buttons.""" + return MessageDialog( message, caption, style | wx.YES_NO ) + + +def YesNoCancelDialog( message, caption, style=wx.ICON_QUESTION ): + """Generic message dialog with yes / no / cancel buttons.""" + return MessageDialog( message, caption, style | wx.YES_NO | wx.CANCEL ) + + +def ImgToBmp( filePath, size ): + """Return a wx bitmap from a filepath, scaled to the toolbar size.""" + img = wx.Image( filePath ) + img.Rescale( size[0], size[1] ) + bmp = wx.BitmapFromImage( img ) + return bmp + + +def BetterBind( self, type, instance, handler, *args, **kwargs ): + self.Bind( type, lambda event: handler( event, *args, **kwargs ), instance ) + + +def IdBind( self, type, id, handler, *args, **kwargs ): + self.Bind( type, lambda event: handler( event, *args, **kwargs ), id=id )