From 1976179dfd350b872816b3bd9c5c3410806dd0b5 Mon Sep 17 00:00:00 2001 From: schroef Date: Thu, 13 Feb 2025 17:41:49 -0400 Subject: [PATCH 1/6] initial commit --- LightEditor.zip | Bin 14991 -> 0 bytes Linking.py | 45 ++++---- icons/select_false.png | Bin 0 -> 563 bytes icons/select_true.png | Bin 0 -> 429 bytes lightEditor.py | 249 +++++++++++++++++++++++++---------------- 5 files changed, 173 insertions(+), 121 deletions(-) delete mode 100644 LightEditor.zip create mode 100644 icons/select_false.png create mode 100644 icons/select_true.png diff --git a/LightEditor.zip b/LightEditor.zip deleted file mode 100644 index db5317f5d7bc10c9b1321d823dc256c6ab987d8c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14991 zcma*O1CS`qmNnY8ZQHhW+P1q-+qP}nwr$(CZS%C=cjkW+bMM4I^Ilf%%&N$UwKG;0 zB6h4LF9i&O0`Rx32+3&v$Ho8K!2l2dNSm9QIf)vXJJ~wWJG(&x0D}CT{EtpW85RJX z^r706x{LNxig1~To46ZY?0zU&gI75CEJAKzaJEm=<1c|o&0 zz`bc`xDB{EP0p)p{8k zma?;dP3PT;{C11D`88#pqX3%~ihqnVfrN3IO)$+VwPOAUTV4trqN8_<;o~31p@9GZ z{)^vxv47tw!vDsvp_Q?Xk+FlGwSkSfiLs*-os+G#)&Gs@mG^4XzmY&!VX6`*BR9wZ z6LR~7s(SO4*Jq3bUP4GGoG}6lDWk+!HBulWd7${`R)QI}pD^J!zt7i(f)>uvJG3y# z?H=He%(8Mc#aLA?uX9Q+Og((T8+bGI(UD&-hBjx4jvO62UUYaKnaM3tUFdB1)308E ztng;gU>;&F3rk$^+#ca<-V|S+k)htmR2I!FDChZJyaOKjoyVI7Q#mZ1p{+ZQwQlPi z_>YGc3rYM5U8aztysx$(uh3+R%on^ET?2iN8nB^r@V_jpCf97d{=>2*;J>k~`R^=a z{Wq4a{_EZNA0FfooEp==vB6hudd+)nvnihN1D@u37Vj$##Iun;HGL4ri@#Ez9K{%O z-I)E}MnGUzPwFTX&y^r~01b3SytV7u?VV70abe3wIgw&&3cpWBhW1g}QPI&+p}w3= zrzWb=n7O>2s#o;%%A+^(eDyL(e_~uelrUaFql%h1Tu9%mf5$@|aRs#V*wC|Csz&a9 z-R+)(TK{uXLAU6&-<@@GS^R8a1&dd!nz+d!$E0h4k*Axi$~tRVYp65go;>WVdDQVx zHD>vpU!x9F+w--`qe%Z0CG~xbuIE!fs%tbXlU46(pFYBlS*xzYaAyCKGTg}JJ-U9C zGP|>bg3r;ET|J2e5LQMpu`p~s4Y#Hd@2E7!Kh@wId>q-JUhBje9-ucCO_!Dt2mmbrM7B zg;(3?QRRVJr>dY(63G(F+)`@J<47j0)~Wj@{?= zy%pS8X6r7&u?a%HQq)tCAKOhdd7v^}2qJF{NV@muJteZlgo~y+%_0FIx@TuOFdLT) zGE!q^m)YnEV-NjJ+HJ&A9Z6oSEv0Sy{H&3thRVg`;UM(#?30UYgz=pc?iJ`4_C8R> zt!h_BmeksaoVht17;wcjfKF8azbUJnl{~q8@WqaiLdMZOQaB3&D^X6-+`5RgVhvJ3 zrFp_IJI^4R0}ddXCn2>0t3K=q|J{1W39+9+Pi;yw5OS`?h$D_Bn#_=GrTRe4Y5_&= znw=PutOU>^TQ++JaGxotpm{$5c!r#0OoQ>0{oCXI@%s3BQNxgV*S!g#9CS zl!gW{(MP{l$_egs_c$O~o&W>%fj)(s#h8FlBWUQcS}HL`X#30x!F-B{Ljgw@PE>t- zq@*o?mtByUr%}A%Y{z`UsijGL89W?lAh8^bAi^83>Yb|!H;z5Fnqg0YjM>N%*h$nH zp^atv;l7_F!1rpT1Ss~21q<4cCP6tDcZPB$P4%xpZwyp{rRiOkva@?;C+TgFF+tG2 zg|O!MO}nkC9Y_KsP&?N5YVk+JZv4$5d-U1y^+!i-PDe@{oc_TOd>wT@*z4n>)P={y z#>7heCCQC?%IX!`)TQ@tDh`JN2a?NO-_A=u~bA>9u!Tf~- zRdML{F2GiEx_K!6I3;`JAR=vesjbxgT^5$w@THFvkIRV0gwb)_8kn+OTpF7yC37P&)z&Dagq8y4$T_~zSI~oP3Wn8TQ zrZ6q4e0*1ud_%$?{|ScFIV7+$w}X7C+P>J_v*DHoCK~eY;pWjADSy3uw6gRsQ0&KJ zC?^9paK-IBD5yPRuI_biY8Id;prP&|J7Jt7QELzLM8JMTHka=DO$)_)O$%RQ9naiu zA7-)UoKr|RRzach9YKxL=4nc1eI69}+XeMKrAl?;@0e2-K?xV5nLlhHCo0Kw4r`Pa z>z1KF_htupBb0|{3ZOkVV|bPA)$}0nRy*Lj^g9H3wsBBU<;fys@_R9pI*Jo|7C{N@ z1hoZCCzDiF3;mi!!?Z)tS2%Tczym5z1Sermt+Wz_X^OZ29!01?3nRK1E#8N;pQ_^# zk39m;nmv8R=W1+BfhCklhS3|4>b%QlSCY_`2WrlID4MkAJavopm=tKxx(11;*2IQ^!R7}x^>mqO|iYVdsld1Av}c#x;!&J?C2+1Te7Dd+u2(Ey=rlB++J@{9kg z-G&?LNs0kSZ__$YXf(ZbPIjVxPB8}06f>%=_18|8%8T1l+(@n!Crs8Xjcm3`yfCfj zZqR4I)!d~9CY{)4)mT~r*3BD)P1z(3phxjfq0q5_KXn1gg3X zLVgQQr!HO&Ds42DEkdfG2Gi5&D$Ikyv;my*2#i|Fd+X-NlwQ%&JFa;p1gO0u^ybp3 znS*&K&ZLZ?UG1*5?#euMm<{_;r0!Zawt=;1W(2stfwqplYJmKJ)0AT&V-N)l#2<@l zj`aGECD7*8d`fZhkpf&HDFJK)t@P6{!u}q!jE(B#7rD|Bdy^o*e)ceh0p4OAp zThE_ZH2^50&$pwCQLuqy1U&UHc6d0;7}eX<#lp)$=D7rWzw`6+@!@5sNXDnBeejQA z{Dx*7r&6tNE)37z^!YOE;4ncgfQL(xY9f>>dp0=vF%81(BC{0aDnkT6XH4HPn+R5$ z0J^kok>-!Yk*EHYMF(zszl>qjDwujnFrDY2k*1jor{w!EwgL+kv(g(L0&4ky z^@h4=cbrH8FSB@m8d7Ot&Krs-VFY9=cU>P^w}9dmcE>7ND`4MXcQ7cj0s#I=29)*1 z#>?~LxoW}@ED5U`zecQrQLy&}wYPfl*9wA^p1GxzH;63e?_&9qZK#`okb518uc%Qe zm*ep~J8p*4$03%a-U=ESxAQ_W0&jyt0GFeXF>exg>U8{Io*-SGgNZD|sFullNl^v{wjpy{M zgZFLlOxVoQ3-36f<2V4|(Z8mj(sXSF;Bu=Q+nzRc$6vFfck_d6U_KJvsC~s6v35B7 z;}coJ7lz`ctCha0NT25uur_8)r_H3M%VrHrNIRlcQB1@Z=uv2XhRn>1Sr2D$ z)>e#U6i@$1qOq?Rptrh2M-fh9JZi>M6@vAQtX`>i<9WvL{i&*q!0n2i0>_1Js3kEv zESm-qqg*(K1O@bE?Wf;TbKj}*+sRXXd95UB z-qr&_w9;A2pmB7BzS4T_Q>A+PPdc|0=swu+#A^9QrgfoOu&4QjY66a7yb7<`zI9NY z@{&&*J$sCtc=2_c5Ai1W+YYjcSd24dPVEM$oFfXQyUofYCw-wtN)3a0FALc$Vwa)X z!LnG~_KeL!8&l8_em)#fWn#xYIb;G|jR|8%PcexPW-XT6nYy;n!F#t&QC7vAx#k+` zH7bf9M^k>wH8`^c+*a2NZA6&f>zN7M&wMw}C_b$0#UktLy~WNX>iykCX39oJ}`xUa!_JaQFkmb2P#Z)zu6rb~Br{ zpq&u5W5e!b-??iVE}Ky0s&Y0CS|Qf_^WjFDL?2Jr-5EBh7QXDI*7K|CIpXGw(%mTh zee4m^U}A+ORMCKICy>YKc%a-HbZylQ`R~Ng)O9zrXu85In)hU}Q;{{9~kv6lL z2-hwa!Dru}%^y|OzQl|q0pS(TKaOS6jI63La>DS%F*~K4ceTsNK5LU(9qgpByL>>% zzU(Gclh@^A6Wq)5(dsbNWlZ;noMZ8?qVFw+(E<)-C68@&`4Edq8Wxabobu96#lW93 zTmj;=to0^KyAb?GgKT~L`Z)n_-{1g45{eq>k}j99j7DLh(h)7R&hitM3Xs&>6xX8seRH}-|7ck;|{jOI2 z){|OnA!!s&9?E-|bo!7J)^@_LW<;#c)&9{x21mGI%nRrq$vtqm(VT7yjV4)E;ajda zN|sQXZ%_`AnP}>mVL@y)Hth)CwzhPE{B{@_UBCQVG|`=Y`)ZI{`YzwRChM<*e~vYR zLk!Oafaci6-bAlYE&KCvMED)KzC@My0?^g0zPBpNmauMfZv{4yzeMMPXLMkVxXQtG z8*f-scy{Lph?ohs=_j2NgbF~V1kXr+GNH9N35k)dUZVjwOW<~qEoA_t>H+E7j>KRp z`gE4X!vN^4%94Gy|3Y~&W>_lFxoVTi{y^M;Gsz-qINBFllcVa8BX{RapoyS|9sjvF z{&afQc^Fq3N;Lv4Ps->t_O6a`a^V8TG1c(+-jxYU5?*EGlKw}c%J0{&J|jU4C>4N5 zrPkv}h(9Br{;;3@0Gci}mDdq+XifuRBrUFOp^^jz>7Jvy>K`;H$03VdvH?>koIK?q zM}o!DArQ25Us%G@DlEHvj@*u{$bgZf>~vc$P}S3QOe4#FNKk6do*vaM+j#5HxM?sn z)V6t^LYvt%`$8v)v;aw~YFA*_v^fn=_Pq%No7|u9U8@%i^-LYTI$0 zSdRx*iEImY(}_Y9_E$@e!;W1gjt8Pq%?f%Rxl%U)tBBUN{xs`2Fy3G`=r`H4(q=Wf@dd$fA=YBQ?j;;qtj&_ zKN@qsV9{1aZq$cy#=c319ae*8hA~KM7oK$cqFPPzS|#Mn5Fl85=dOGc;2m$+gw2cKNc|eIyq5Q?iBgr+4#fGbD3 zj_rQ0DRN3J%IuaeM8=lnGyiTE)o17*W5NA#?A-imySZq&@!hrFYy4ml*`2*v{O8o^ zkURJFAutCkIA|B>3!xya8Y?5~9MPoZljTb{PzjeEu z6QtG<)q8lXLpviZ%(x{fSV12uV|bbv6NHB*IPcW}&I z&{};a{3|X}*mYW?C0gfE$Nu;AYyQ{r%{_#*dr=o4U9}TRSwx7-a41(N(kg8fT7wNk zOHZ47btPLNi}4@!d>mZ-!%`xxJa!TjxY2|KwEWEFI1zDzfdDLwp&1G^q;GvUp6-l; z9uAmyP_5grxpssVSnz4D*SupxK^s;u(t@W#Q+z(aE;I?9`o24-P^X-VzC{!3P!2Qc zmbXIx5nU#fXk{&tC~EH8zS#Yppgro-Ke?q>g(l@P@pCq@i<6U&QWptu!Z@0|^X)B< zhS}sc7V4F48{QPOcp;ksEl{Mb@h?Jns!l*kC$^BzuOE!{HOrTu<=L0Y?4k(Uj*R!yxf(P zlhvuSpFyZXM^E{4aRY)rIsfdVMCqhjFZWji`6da`UBG3FLX0FXd?S9hC~YXSB7oVC z6vE4YWsbk%QRRQgH)#y?WP4o+-7vg@HVoFCD?&iM1o_UCsorr>T1 zYpDz?(v%Y5ud%g90ag_p6YMd$@RGIT#{NoVC6-9b9*T+1zPphjWIbH1>&}-#+xzvs zah2HJZ27E_Cah&%MVWj^>cpe2-V+jratTLFa$|C~Cw0_%|wnE6P zh}4~Pk(dM!l#m)~F2v4!JZr}0J@6b}7fQLGsKlU~+U4?XS(rl?98?i;4u9^ZU@&!a zN3ynyvB@8s9Q49cnp^z=4d_$VGpOBdnOzO#7u4ogLBTyLUGFT3M8t?ojWH4T#ZO%%(bM%vN&J$!Xlio11?TVUJMU#K0z_I2`UmS87|ugi1>(sxZa!DV4FFBm7VG1x*@;# zrDJ`(T)rdO+Ls}WOfL@(DOJxh!tK$_34|3g-%zL-+m>5IO~9GcxOpL?OJg$&?jdL+ z0@VIaFXn&Wm>g_kA)M-b%5)Q1RNH~gj=e0D8z z+HVX#Fjm>seMcHwiv}X=*hoh@nX!Ntg~{Dj!W=x!@mv8;V)A06HhHru%z}(_DatE9 zzZQ^4TztbtW%Y6qSfWK``)AtvpXs*oKfRF;A+z20F9f7;x!V*DZ&syK`^c(iC%_vF zWAJK}rmxyQ7_h9dOBGYRet3tj}9&28n&v)lK`3zQeG&*_5^(e>#=+?$*V^ zp)Ype9m=o!Q^fJbQqBudPVp6b^{Y>3- z^1fKsn@Gg3UU_FJ<$YS;`{b&C<^=FL+R(LvyIZm{>taP6C} zZvSq-2_O%|H>uR}E0s01ABjK`=1PeM6$O7u9};o*!~fbb?PCJh=<1R%ROK-aevDtw zoGJO*8imL6lLCXlUeAkR?M+WJ&poG+bs$6F0wBhj-nqbeYAazi6_H(q&#VzaR(uc&oC=eS{~7gBDu`dUu@b5aJey#GHnO#sA-=o9He;wd&3J- z-c$XAnD5-mFbhKxER2DiVHqtW>UmXMhrqZegb+8{s73ZWV%Pxqime8~b3nN%nkl3O zYi9sdHLi&%zl8<&+)&QnX&6d21y=z*Vx+&dr|NoCqq`j%KP@IkwKsIXJku3N7^UMy zs1;gZ$@x^Yjh}PV1d!AaW01WFECY2VVP3}EWXQKq*celq8k)Wp7&Qh<-L<3c`m7DS z3a+PqlT z*_JJR!QneM)tM6h7ni@M-zOD{I>khwojT^8PkgfH70S7ty&Jcxt4T7F#zwy3ek@VT z7x%7=`e$;Na*(WB0(y6u6IY_5mqL+Sa?A|n$OyQqVv~NPz3b_;aO;Lr${;T&aZo<~ zhX?cJgOHItz1vRmg1$HQ>H!1l zX$0e>y|`;U4hS7jA(yU4ehCGxe+9;_CR|2G8|G9)gafw-tEE4&?5cBy3i2>pd|8k? z+3!S;6Ijy_ed4w2IXpD~Lst${XOB#VXf1Jc*~!akU?U#8xh=@rw;bF;P5=ge>WC`T15?+lQ54&4YyTJ^co;id z>|}Evp$Mp&jFV-}tv`Slyq~!*FlLmNE%(UR(d@23FW=0|(^YmW(@Jdhc`CQ*nSgk% z-zHh#>U9M}o>MzeIJf-X>MG`xj+NP!sb{Kf#ceash(Ic1 z3fwf3uhDW!*ozFp7fAUwl2C?-R*btI-utvNAU@qgSWqIc7%d_hk^GG^&9KG@Noqe;#A2jtYexTy$4i3KJ~6>R~3W# zml@8f?-E#jh~rB`wFH-LfhJ#y=o&=1E`LXdRLL`M^0__0{*iq)t(YMUM9z z8V^h5;QN~ywSB)+WK`0IC4!xzU% z2%`gR8zH*C)kP%81lLaNsmh86kQcjYBOhCfji#BPU)V$w-PcJwu?vxC^BaQI+`{3y zIjUzxVRUtv3(T3NCdNak7Am>b9CCB1`DIN%u&-uSfxhkC>A8~s+vi=B%QPM$DU8TI z&Pqftv(R5VNG#3gefS6PuV96@Jnu+?2mk=C^IruknhO6eSV8-5f)#0V8%uK=)Bi!N z;ssq}`ZprD{t>I-uG?Ub-*5Z<26RyvAk<2)gBXp^A@XC4=!>uoYrvbP zagsoX_^meEakgvl@_0Wyjq}K) zyuC80SBFFr5~AdYJC5*&Ar|79S(n4OMX{L z6qiM08}Q^$;>sd3h)L=$l#s~-@tX)R!F-V?1ZNL)I+8^!4Ur+8#IXhLeK5jAcsMj) zR4m+3WXE?e771__UJnu4E z0s+mG$UO~>8GbgW@JA{cD3wu+M<~HwKqy8-zau+n3V6ImC!OB@@bUU`i+fdZJ_lD1=$(OzuNWB5i$;BwbwDQCV6nr$m)< zqfZ-!b=q~O1F{kliBi6Sl8%eQh-w&Z6IBpF*BX>w)flRLf-?HAgief=uoeO9KX<8$ zUC$``>wHIignP)YW*5*I1Iep!ftjO-a9_~-&3-M~Va*dHONILhFrj7zLFU*{&4X@# zR`HCUA1z;23U9g)GZM28=(9ckS-!C{WB~I&%#Fbl$GqG#Tl{P=T1PO1kqf{^pyZ{K z9b*0iDfS)K=!hso5k)$iGsy}F`0AnUU=ozhZlf9N0sNplU1F`n$6&B>WB|!#B6pA{ zu(^<@*@70remJ>m-H?~>sa()^?jRXJ)UJRqC+w684BA8lM&-4g$gl=s6<@bm8Q{-r zWCzLuE=+s00XB2+?Aq_K<8gtywL z1PB#&c3;O8@iru*AfOv;pAQ*J8h&;^Mf-G zNdVqK7}zCgV@oUvG*hbQ`)*6taG>My%>={U`@lJX6p9uh(LiEqOEjh$g!9t&D&UQE z0u!JocGunDiL%8|IfvZXQRR}Zpq^!y{FtTSGQ^@W3E>bGWoPgxn#L%I2q3}+1^;Qo zNOT#oxY!wUXsF@BvP_QM&2HR}>^~x~1{im!TgzX^%?J)6V-{iAfs>n?6BAoYc51q& zKq+3_DSrF$CrJVNjGT+s;R3Mm1T|Cpf~gTo5q7VW@SDR){h^32#sZoE=bgNycET>2 zMH!B`yivPV-9906c5Bc^K#;$J46D8LBmzoP`=FJPQ6{NFdX}v!);b|Q+Rs3aG6l42 zHuP3kJ4bQ9HQ=4)0iqVx2%z{g00LtpZVq5fiuquoSpY#}zAq z=*X8iK!Oi>AG~KLTLGF>9Zn>te@dgWqz{pI`M5qm1{jvgn2-TDxs?Z>oCpcs5iHFF zu#9m7%h4BAv5d7F!faD5ZF=+Xn}&fh{ioF|sqUOyQftGyoP*X>g1bV~oamsR66cUg zB4#QefqgjGn)rtJqB-77ZpiDy(HNh1Ls6kwt6_#?|wd^~X3uYHh}}?{j~jSgtdk zDq~%%N5WW{jb22U3guIqOi8@jY7kY{8)7RD4kfiV7#PM=u|@IkCc`eaw7VLNHC3ph zTe@6b*krKKVy{$Z3LVwidst7SJvdxuKCz716tcq?gl&N=#zNqM>&bDnG%yfY@69kD z0>_Pji+D;oR-?_^3+q9s8_NLbU77=Dhal?%%>0OBWP?`9HaMh5r*}dOTbH01)AzR- zbfY?=%V3i|=&Imy7pEBuX)J_^b$~4FQ9E^Ul=KL^!!sb*_iwXhv6PatpfW6HJv&Ie97lfcCiJMuO==)u4TW1oWb4jNm_DHJ=EyiAr%+gwe zaLjb3PV}UDSsQl~mZ5%9C(+@q-)W##lfOnxL!2DN5My1Xszu)EAC`*!sI6R?f}*BI zjYxbeQr!d$-kYvUi6u}D(#R%XLAwH#Tw#zbU%s*OOO49W|0Pek`GdrBp3BQr9Oz#Efw(VaMdfxeT;i zX@>W;wsHP2&E~3_M$1l^c5O?+w;~~DPG%D56S1Ln$6B-LgRt|=7yV!-0TsbTRZ8OA zvKv(DSB3bcDrkHr&*luf+yj9jn_TKQzD)g*OPO;o>NHKx)Lqg8 zXO~A4N&z_-n1RoE`kG;0Oy+p};~Vv0wRpp%g*J~Q0UYk+Vkp*hQcWo5uY8ab3qJYtw$I$Ar4iSt+H?vC-7` zAdb7(1)N=aQ7k>(3_ZvGA^jz^Q8RDI?K*rFf)G#QoS310_U zG$p@3TBWgL7gwGy)t9=HNqd4vf-gq=DV*hFPauVF-*i5fgbU^jz8INJYJc_Yw4MI; zKFmw#Xkgk7fLy;LjX<}s z7>O2mI#}M!SvF3RHp=9112%A%({_#P!SfsdSg+onW3{01o{an9P%Lxj0+gHzLftr} z3NfgXIf2f%#(`Nlpgv6CZk(Z6vnDa+1w3gxd>9fbwecwvDXaU|1jO8EcV9&Lj%xwm z=|B6&L}{e2Vu0~+)CdS+h!gdbalzv%{IwlhD4g^?fWm5kJViK3Du)LOeo#DSb4MV5SQ#@%#%k4fSkl2D63$0|$?q)1w@YS|geS9@=kLB4=qXVQ zEjtsK@YEU4%X0Lj%J}-muM|eqy`=HC(Oh_N4);`4Wh$-YA5_rWn#;*BN2IdN^T3vm z1!>Wq;pfLxwzI*cgCOHLYcp%VsY16ezgsJgUXL}|cEnhkkFu-II4kzbsJyyrtU9Z@ zYzBLNOxSoLm856ZDz7;oLt~@6fg)R8b{TraR&80Px;^Xbrg(N9<7Q8E?@D!Fg7WW5 zb?q7+5O(q|fEe`_Z^Oke{3+h%%C%2fdjBHIF>mBt3iK0By|sP#&@1-ZIzYoNWD4r- zq~t$IaP~%$*QmMywf0_EE@=6zLT)+;)x%@E?Z)|W`nm|avumcPtS`3sEk6N#E)#iv zO|@E|wnK=ZO`P!It-M@Ies!_%I|_x8zFJRKX;0h^dEQ&c(~~Ir(QRA%s#9~YqXs(gX(u>teu+*^aqlI+^!Pf z43eX8l$U`?3JB5N*`Zj4R^NBbkFAZWi7|pyhV%+U2-?(~SFau>0TF8ppQjE28v(8H zS@L3{n<79327j^A>{s3NB`{@QO(DfEU(mQKxu+^$o*9Locvj*lpn|PFM!)6%>_~De zF_h^7akgVRU4KJDs&`r&>ZEPZLLzMhf4LYfQ&o+!aTXA-4t*S>)+}R}+ekO=S-3)0 zK`MGe_7Km?SFRqWNhC|U1&~RzTm?>wTiY= zs=+UyCY6m}+(gJ3Mj&QZ*CP?h^t?_GP&dnc(BIS!uJ;Oqyg9owa^QNQkEJKGU6+V0 zmHRILIl~z_K>)1tX43oQ%4VldnFPno<+eFx^s$Nj(70CE#VFO`Z_;Fjw^CIyLm*i#iU6J6e{hslgKj|v%C=V4+#_})=SzNB} zs+h2#p%<;dJQC5?{iDcSmV@@V0ab{&yye!{yyR9ibIygg`Bg1FX*Tx&d+1KOhgJ=q zAU@{+mQ?u&prxCWs}p}Y$=L3}>+cIcwwudKe8@qqIN{qXxB(r|d)z0xxUilpS=A5$ zkQUUF_fjf7C3yLq`<^CtB?L=YtyvPorFKMEgy@hp%jM1>WL8(9pxx2Z{%d1an1U>g zpR|NplvmlBzl~VFT=kiOntk3m$g0AWntX`X4eE(xhMGU*<;s0?s}=09E#1~p-#AAF zT67$7rErQz`a=Wmx@^46Rh7n;;9Tm2CU=OKjoCgH9-@pxH(g_pvwGHd36E~j?MeFw z&kO5I#fmhgQ~PDM$w9AEvO)W%c(X@Qsfehi4C%&@wNm&uKj~idwpIFd05*Cj@Xd-% z3qbQ88W7QYb$Uk94&dPV6f#@sEelt~XvjKZN#gayemHSGM|I>x>%Del1aK}N7IfPQQ&-26|;J;Naj|D}i03-l_7uUa4E@ z@bB=y+7>|p0RN9&@&6$Hr(yA5t#;@D|ApA%FZtgP|2O;MKj43@i~qUYzu}vm77sUFvGzh@|p636A&OuFSW*hu>aN><)uKu{_Yj%KgYs9B$RUez59OvA;>2P diff --git a/Linking.py b/Linking.py index a3d4e5d..2527631 100644 --- a/Linking.py +++ b/Linking.py @@ -3,6 +3,22 @@ from bpy.app.handlers import persistent +# DONT USE THIS!!! +# class LL_OT_LoadLightEditor(bpy.types.Operator): +# """Load the original Light Editor UI""" +# bl_idname = "light_link.load_light_editor" +# bl_label = "Load Light Editor" + +# def execute(self, context): +# import importlib +# from . import lightEditor # Adjust the import as needed +# importlib.reload(lightEditor) +# # Unregister the current Linking panel so the Light Editor can be shown. +# bpy.utils.unregister_class(LL_PT_Panel) +# lightEditor.register() +# return {'FINISHED'} + + # ------------------------------------------------------------------- # Helper: Get Selected Collections from the Outliner @@ -361,18 +377,6 @@ def execute(self, context): self.report({'WARNING'}, "No lights selected") return {'CANCELLED'} - # Gather selected meshes and meshes from selected collections - selected_meshes = [item.obj for item in scene.ll_mesh_items if item.selected and item.obj] - collection_meshes = [] - for item in scene.ll_collection_items: - if item.selected and item.coll: - for obj in item.coll.all_objects: - if obj.type == 'MESH': - collection_meshes.append(obj) - - # Combine meshes uniquely by name - all_meshes = {obj.name: obj for obj in (selected_meshes + collection_meshes)}.values() - total_removed = 0 for light in selected_lights: group_name = light.get("light_linking_receiver_collection") @@ -383,7 +387,7 @@ def execute(self, context): continue removed = 0 for obj in list(linking_group.objects): - if obj.name in [m_obj.name for m_obj in all_meshes]: + if obj.name in [m_obj.name for m_obj in scene.ll_mesh_items if m_obj.selected]: linking_group.objects.unlink(obj) removed += 1 total_removed += removed @@ -479,18 +483,6 @@ def execute(self, context): self.report({'WARNING'}, "No lights selected") return {'CANCELLED'} - # Gather selected meshes and meshes from selected collections - selected_meshes = [item.obj for item in scene.ll_mesh_items if item.selected and item.obj] - collection_meshes = [] - for item in scene.ll_collection_items: - if item.selected and item.coll: - for obj in item.coll.all_objects: - if obj.type == 'MESH': - collection_meshes.append(obj) - - # Combine meshes uniquely by name - all_meshes = {obj.name: obj for obj in (selected_meshes + collection_meshes)}.values() - total_removed = 0 for light in selected_lights: group_name = light.get("shadow_linking_blocker_collection") @@ -505,7 +497,7 @@ def execute(self, context): removed = 0 for obj in list(linking_group.objects): - if obj.name in [m_obj.name for m_obj in all_meshes]: + if obj.name in [m_obj.name for m_obj in scene.ll_mesh_items if m_obj.selected]: try: linking_group.objects.unlink(obj) removed += 1 @@ -556,6 +548,7 @@ class LL_PT_Panel(bpy.types.Panel): bl_space_type = "VIEW_3D" bl_region_type = "UI" bl_category = "Light Editor" + bl_options = {'DEFAULT_CLOSED'} def draw(self, context): layout = self.layout diff --git a/icons/select_false.png b/icons/select_false.png new file mode 100644 index 0000000000000000000000000000000000000000..e8d2524788c7c27d76e652a7902d0002b7c3e035 GIT binary patch literal 563 zcmV-30?hr1P)A^nB6@J1rKN53GnP8>L^6UK@3)FbH^ZOXWY;9ec2|Jr2;>d1SM2XcBPF?UE5 zoc{(4<*KG6jmOaL!kUOgG?Q{&nlmQ{tYNst_znoI$_q%c)ad7BM~y_Zc0i3EWP>#~ zl7RQ#s5l41g>3q+1wWK3CwB~NM`$k>Wd9&!tUFTR8x6=xJX-#31E3BoDj}?L5?W zfVihs+}%*z5_eo>!jPK3YHL~c@VU|0>R$i{xRlwnp>Z|*sqm(BTh)SqcHbf)i&xXy zx)_0o1sfz)4~nXj^JMe*QyklV%@X3cfvQix%rAaUF2>(l{SDdF0TsSc0b?rOiCc1b zl)6xQAjYpU=-4u;9OLzSsYo4h?9kQc_2%*|zyKrWp4r<~^7jA$002ovPDHLkV1mB6 B0UH1S literal 0 HcmV?d00001 diff --git a/icons/select_true.png b/icons/select_true.png new file mode 100644 index 0000000000000000000000000000000000000000..daa3cf512a93a659adf3b1e445ab5d4b9946ff6c GIT binary patch literal 429 zcmV;e0aE^nP)Af1otTuRC@d$O4l=op$q7d9gh8+6c|QwbzORJJ}j(0d2`=c}Ntk*3&H6=aHh z-xT4`jRBB2d!k>0G*_F%)QP}`B^Gf9-H;sUAR?)60H@k~{%9&*03hc_3}uZ>sqYl* zLiYi|%V!t>NJHf)3Z+RE2nPU;+5mX(-&%Ao7+Iq}u!deOIIs9EvqnV36AOe~s{W+3 zPD3sn)(K=FFRy0W$1xA2z9A=M)Q0cwg^f?P!_!iB#u0R&Fp*#nD$+R see @persistent @@ -1026,8 +1081,13 @@ def register(): # bpy.app.handlers.depsgraph_update_post.append(LE_check_lights_enabled) bpy.app.handlers.load_post.append(LE_check_lights_enabled) + + def unregister(): + global custom_icons + bpy.utils.previews.remove(custom_icons) + #removove handler post load > see @persistent bpy.app.handlers.load_post.remove(LE_clear_handler) bpy.app.handlers.load_post.remove(LE_check_lights_enabled) @@ -1035,8 +1095,7 @@ def unregister(): # Unregister scene properties del bpy.types.Scene.current_active_light del bpy.types.Scene.current_exclusive_group - del bpy.types.Scene.selected_render_layer - + # Unregister other classes and properties for cls in reversed(classes): bpy.utils.unregister_class(cls) From 96f07af1a2fba6fb5ba8691d29928b89ec4e911a Mon Sep 17 00:00:00 2001 From: schroef Date: Thu, 13 Feb 2025 17:49:22 -0400 Subject: [PATCH 2/6] Revert "initial commit" This reverts commit 1976179dfd350b872816b3bd9c5c3410806dd0b5. --- LightEditor.zip | Bin 0 -> 14991 bytes Linking.py | 45 ++++---- icons/select_false.png | Bin 563 -> 0 bytes icons/select_true.png | Bin 429 -> 0 bytes lightEditor.py | 249 ++++++++++++++++------------------------- 5 files changed, 121 insertions(+), 173 deletions(-) create mode 100644 LightEditor.zip delete mode 100644 icons/select_false.png delete mode 100644 icons/select_true.png diff --git a/LightEditor.zip b/LightEditor.zip new file mode 100644 index 0000000000000000000000000000000000000000..db5317f5d7bc10c9b1321d823dc256c6ab987d8c GIT binary patch literal 14991 zcma*O1CS`qmNnY8ZQHhW+P1q-+qP}nwr$(CZS%C=cjkW+bMM4I^Ilf%%&N$UwKG;0 zB6h4LF9i&O0`Rx32+3&v$Ho8K!2l2dNSm9QIf)vXJJ~wWJG(&x0D}CT{EtpW85RJX z^r706x{LNxig1~To46ZY?0zU&gI75CEJAKzaJEm=<1c|o&0 zz`bc`xDB{EP0p)p{8k zma?;dP3PT;{C11D`88#pqX3%~ihqnVfrN3IO)$+VwPOAUTV4trqN8_<;o~31p@9GZ z{)^vxv47tw!vDsvp_Q?Xk+FlGwSkSfiLs*-os+G#)&Gs@mG^4XzmY&!VX6`*BR9wZ z6LR~7s(SO4*Jq3bUP4GGoG}6lDWk+!HBulWd7${`R)QI}pD^J!zt7i(f)>uvJG3y# z?H=He%(8Mc#aLA?uX9Q+Og((T8+bGI(UD&-hBjx4jvO62UUYaKnaM3tUFdB1)308E ztng;gU>;&F3rk$^+#ca<-V|S+k)htmR2I!FDChZJyaOKjoyVI7Q#mZ1p{+ZQwQlPi z_>YGc3rYM5U8aztysx$(uh3+R%on^ET?2iN8nB^r@V_jpCf97d{=>2*;J>k~`R^=a z{Wq4a{_EZNA0FfooEp==vB6hudd+)nvnihN1D@u37Vj$##Iun;HGL4ri@#Ez9K{%O z-I)E}MnGUzPwFTX&y^r~01b3SytV7u?VV70abe3wIgw&&3cpWBhW1g}QPI&+p}w3= zrzWb=n7O>2s#o;%%A+^(eDyL(e_~uelrUaFql%h1Tu9%mf5$@|aRs#V*wC|Csz&a9 z-R+)(TK{uXLAU6&-<@@GS^R8a1&dd!nz+d!$E0h4k*Axi$~tRVYp65go;>WVdDQVx zHD>vpU!x9F+w--`qe%Z0CG~xbuIE!fs%tbXlU46(pFYBlS*xzYaAyCKGTg}JJ-U9C zGP|>bg3r;ET|J2e5LQMpu`p~s4Y#Hd@2E7!Kh@wId>q-JUhBje9-ucCO_!Dt2mmbrM7B zg;(3?QRRVJr>dY(63G(F+)`@J<47j0)~Wj@{?= zy%pS8X6r7&u?a%HQq)tCAKOhdd7v^}2qJF{NV@muJteZlgo~y+%_0FIx@TuOFdLT) zGE!q^m)YnEV-NjJ+HJ&A9Z6oSEv0Sy{H&3thRVg`;UM(#?30UYgz=pc?iJ`4_C8R> zt!h_BmeksaoVht17;wcjfKF8azbUJnl{~q8@WqaiLdMZOQaB3&D^X6-+`5RgVhvJ3 zrFp_IJI^4R0}ddXCn2>0t3K=q|J{1W39+9+Pi;yw5OS`?h$D_Bn#_=GrTRe4Y5_&= znw=PutOU>^TQ++JaGxotpm{$5c!r#0OoQ>0{oCXI@%s3BQNxgV*S!g#9CS zl!gW{(MP{l$_egs_c$O~o&W>%fj)(s#h8FlBWUQcS}HL`X#30x!F-B{Ljgw@PE>t- zq@*o?mtByUr%}A%Y{z`UsijGL89W?lAh8^bAi^83>Yb|!H;z5Fnqg0YjM>N%*h$nH zp^atv;l7_F!1rpT1Ss~21q<4cCP6tDcZPB$P4%xpZwyp{rRiOkva@?;C+TgFF+tG2 zg|O!MO}nkC9Y_KsP&?N5YVk+JZv4$5d-U1y^+!i-PDe@{oc_TOd>wT@*z4n>)P={y z#>7heCCQC?%IX!`)TQ@tDh`JN2a?NO-_A=u~bA>9u!Tf~- zRdML{F2GiEx_K!6I3;`JAR=vesjbxgT^5$w@THFvkIRV0gwb)_8kn+OTpF7yC37P&)z&Dagq8y4$T_~zSI~oP3Wn8TQ zrZ6q4e0*1ud_%$?{|ScFIV7+$w}X7C+P>J_v*DHoCK~eY;pWjADSy3uw6gRsQ0&KJ zC?^9paK-IBD5yPRuI_biY8Id;prP&|J7Jt7QELzLM8JMTHka=DO$)_)O$%RQ9naiu zA7-)UoKr|RRzach9YKxL=4nc1eI69}+XeMKrAl?;@0e2-K?xV5nLlhHCo0Kw4r`Pa z>z1KF_htupBb0|{3ZOkVV|bPA)$}0nRy*Lj^g9H3wsBBU<;fys@_R9pI*Jo|7C{N@ z1hoZCCzDiF3;mi!!?Z)tS2%Tczym5z1Sermt+Wz_X^OZ29!01?3nRK1E#8N;pQ_^# zk39m;nmv8R=W1+BfhCklhS3|4>b%QlSCY_`2WrlID4MkAJavopm=tKxx(11;*2IQ^!R7}x^>mqO|iYVdsld1Av}c#x;!&J?C2+1Te7Dd+u2(Ey=rlB++J@{9kg z-G&?LNs0kSZ__$YXf(ZbPIjVxPB8}06f>%=_18|8%8T1l+(@n!Crs8Xjcm3`yfCfj zZqR4I)!d~9CY{)4)mT~r*3BD)P1z(3phxjfq0q5_KXn1gg3X zLVgQQr!HO&Ds42DEkdfG2Gi5&D$Ikyv;my*2#i|Fd+X-NlwQ%&JFa;p1gO0u^ybp3 znS*&K&ZLZ?UG1*5?#euMm<{_;r0!Zawt=;1W(2stfwqplYJmKJ)0AT&V-N)l#2<@l zj`aGECD7*8d`fZhkpf&HDFJK)t@P6{!u}q!jE(B#7rD|Bdy^o*e)ceh0p4OAp zThE_ZH2^50&$pwCQLuqy1U&UHc6d0;7}eX<#lp)$=D7rWzw`6+@!@5sNXDnBeejQA z{Dx*7r&6tNE)37z^!YOE;4ncgfQL(xY9f>>dp0=vF%81(BC{0aDnkT6XH4HPn+R5$ z0J^kok>-!Yk*EHYMF(zszl>qjDwujnFrDY2k*1jor{w!EwgL+kv(g(L0&4ky z^@h4=cbrH8FSB@m8d7Ot&Krs-VFY9=cU>P^w}9dmcE>7ND`4MXcQ7cj0s#I=29)*1 z#>?~LxoW}@ED5U`zecQrQLy&}wYPfl*9wA^p1GxzH;63e?_&9qZK#`okb518uc%Qe zm*ep~J8p*4$03%a-U=ESxAQ_W0&jyt0GFeXF>exg>U8{Io*-SGgNZD|sFullNl^v{wjpy{M zgZFLlOxVoQ3-36f<2V4|(Z8mj(sXSF;Bu=Q+nzRc$6vFfck_d6U_KJvsC~s6v35B7 z;}coJ7lz`ctCha0NT25uur_8)r_H3M%VrHrNIRlcQB1@Z=uv2XhRn>1Sr2D$ z)>e#U6i@$1qOq?Rptrh2M-fh9JZi>M6@vAQtX`>i<9WvL{i&*q!0n2i0>_1Js3kEv zESm-qqg*(K1O@bE?Wf;TbKj}*+sRXXd95UB z-qr&_w9;A2pmB7BzS4T_Q>A+PPdc|0=swu+#A^9QrgfoOu&4QjY66a7yb7<`zI9NY z@{&&*J$sCtc=2_c5Ai1W+YYjcSd24dPVEM$oFfXQyUofYCw-wtN)3a0FALc$Vwa)X z!LnG~_KeL!8&l8_em)#fWn#xYIb;G|jR|8%PcexPW-XT6nYy;n!F#t&QC7vAx#k+` zH7bf9M^k>wH8`^c+*a2NZA6&f>zN7M&wMw}C_b$0#UktLy~WNX>iykCX39oJ}`xUa!_JaQFkmb2P#Z)zu6rb~Br{ zpq&u5W5e!b-??iVE}Ky0s&Y0CS|Qf_^WjFDL?2Jr-5EBh7QXDI*7K|CIpXGw(%mTh zee4m^U}A+ORMCKICy>YKc%a-HbZylQ`R~Ng)O9zrXu85In)hU}Q;{{9~kv6lL z2-hwa!Dru}%^y|OzQl|q0pS(TKaOS6jI63La>DS%F*~K4ceTsNK5LU(9qgpByL>>% zzU(Gclh@^A6Wq)5(dsbNWlZ;noMZ8?qVFw+(E<)-C68@&`4Edq8Wxabobu96#lW93 zTmj;=to0^KyAb?GgKT~L`Z)n_-{1g45{eq>k}j99j7DLh(h)7R&hitM3Xs&>6xX8seRH}-|7ck;|{jOI2 z){|OnA!!s&9?E-|bo!7J)^@_LW<;#c)&9{x21mGI%nRrq$vtqm(VT7yjV4)E;ajda zN|sQXZ%_`AnP}>mVL@y)Hth)CwzhPE{B{@_UBCQVG|`=Y`)ZI{`YzwRChM<*e~vYR zLk!Oafaci6-bAlYE&KCvMED)KzC@My0?^g0zPBpNmauMfZv{4yzeMMPXLMkVxXQtG z8*f-scy{Lph?ohs=_j2NgbF~V1kXr+GNH9N35k)dUZVjwOW<~qEoA_t>H+E7j>KRp z`gE4X!vN^4%94Gy|3Y~&W>_lFxoVTi{y^M;Gsz-qINBFllcVa8BX{RapoyS|9sjvF z{&afQc^Fq3N;Lv4Ps->t_O6a`a^V8TG1c(+-jxYU5?*EGlKw}c%J0{&J|jU4C>4N5 zrPkv}h(9Br{;;3@0Gci}mDdq+XifuRBrUFOp^^jz>7Jvy>K`;H$03VdvH?>koIK?q zM}o!DArQ25Us%G@DlEHvj@*u{$bgZf>~vc$P}S3QOe4#FNKk6do*vaM+j#5HxM?sn z)V6t^LYvt%`$8v)v;aw~YFA*_v^fn=_Pq%No7|u9U8@%i^-LYTI$0 zSdRx*iEImY(}_Y9_E$@e!;W1gjt8Pq%?f%Rxl%U)tBBUN{xs`2Fy3G`=r`H4(q=Wf@dd$fA=YBQ?j;;qtj&_ zKN@qsV9{1aZq$cy#=c319ae*8hA~KM7oK$cqFPPzS|#Mn5Fl85=dOGc;2m$+gw2cKNc|eIyq5Q?iBgr+4#fGbD3 zj_rQ0DRN3J%IuaeM8=lnGyiTE)o17*W5NA#?A-imySZq&@!hrFYy4ml*`2*v{O8o^ zkURJFAutCkIA|B>3!xya8Y?5~9MPoZljTb{PzjeEu z6QtG<)q8lXLpviZ%(x{fSV12uV|bbv6NHB*IPcW}&I z&{};a{3|X}*mYW?C0gfE$Nu;AYyQ{r%{_#*dr=o4U9}TRSwx7-a41(N(kg8fT7wNk zOHZ47btPLNi}4@!d>mZ-!%`xxJa!TjxY2|KwEWEFI1zDzfdDLwp&1G^q;GvUp6-l; z9uAmyP_5grxpssVSnz4D*SupxK^s;u(t@W#Q+z(aE;I?9`o24-P^X-VzC{!3P!2Qc zmbXIx5nU#fXk{&tC~EH8zS#Yppgro-Ke?q>g(l@P@pCq@i<6U&QWptu!Z@0|^X)B< zhS}sc7V4F48{QPOcp;ksEl{Mb@h?Jns!l*kC$^BzuOE!{HOrTu<=L0Y?4k(Uj*R!yxf(P zlhvuSpFyZXM^E{4aRY)rIsfdVMCqhjFZWji`6da`UBG3FLX0FXd?S9hC~YXSB7oVC z6vE4YWsbk%QRRQgH)#y?WP4o+-7vg@HVoFCD?&iM1o_UCsorr>T1 zYpDz?(v%Y5ud%g90ag_p6YMd$@RGIT#{NoVC6-9b9*T+1zPphjWIbH1>&}-#+xzvs zah2HJZ27E_Cah&%MVWj^>cpe2-V+jratTLFa$|C~Cw0_%|wnE6P zh}4~Pk(dM!l#m)~F2v4!JZr}0J@6b}7fQLGsKlU~+U4?XS(rl?98?i;4u9^ZU@&!a zN3ynyvB@8s9Q49cnp^z=4d_$VGpOBdnOzO#7u4ogLBTyLUGFT3M8t?ojWH4T#ZO%%(bM%vN&J$!Xlio11?TVUJMU#K0z_I2`UmS87|ugi1>(sxZa!DV4FFBm7VG1x*@;# zrDJ`(T)rdO+Ls}WOfL@(DOJxh!tK$_34|3g-%zL-+m>5IO~9GcxOpL?OJg$&?jdL+ z0@VIaFXn&Wm>g_kA)M-b%5)Q1RNH~gj=e0D8z z+HVX#Fjm>seMcHwiv}X=*hoh@nX!Ntg~{Dj!W=x!@mv8;V)A06HhHru%z}(_DatE9 zzZQ^4TztbtW%Y6qSfWK``)AtvpXs*oKfRF;A+z20F9f7;x!V*DZ&syK`^c(iC%_vF zWAJK}rmxyQ7_h9dOBGYRet3tj}9&28n&v)lK`3zQeG&*_5^(e>#=+?$*V^ zp)Ype9m=o!Q^fJbQqBudPVp6b^{Y>3- z^1fKsn@Gg3UU_FJ<$YS;`{b&C<^=FL+R(LvyIZm{>taP6C} zZvSq-2_O%|H>uR}E0s01ABjK`=1PeM6$O7u9};o*!~fbb?PCJh=<1R%ROK-aevDtw zoGJO*8imL6lLCXlUeAkR?M+WJ&poG+bs$6F0wBhj-nqbeYAazi6_H(q&#VzaR(uc&oC=eS{~7gBDu`dUu@b5aJey#GHnO#sA-=o9He;wd&3J- z-c$XAnD5-mFbhKxER2DiVHqtW>UmXMhrqZegb+8{s73ZWV%Pxqime8~b3nN%nkl3O zYi9sdHLi&%zl8<&+)&QnX&6d21y=z*Vx+&dr|NoCqq`j%KP@IkwKsIXJku3N7^UMy zs1;gZ$@x^Yjh}PV1d!AaW01WFECY2VVP3}EWXQKq*celq8k)Wp7&Qh<-L<3c`m7DS z3a+PqlT z*_JJR!QneM)tM6h7ni@M-zOD{I>khwojT^8PkgfH70S7ty&Jcxt4T7F#zwy3ek@VT z7x%7=`e$;Na*(WB0(y6u6IY_5mqL+Sa?A|n$OyQqVv~NPz3b_;aO;Lr${;T&aZo<~ zhX?cJgOHItz1vRmg1$HQ>H!1l zX$0e>y|`;U4hS7jA(yU4ehCGxe+9;_CR|2G8|G9)gafw-tEE4&?5cBy3i2>pd|8k? z+3!S;6Ijy_ed4w2IXpD~Lst${XOB#VXf1Jc*~!akU?U#8xh=@rw;bF;P5=ge>WC`T15?+lQ54&4YyTJ^co;id z>|}Evp$Mp&jFV-}tv`Slyq~!*FlLmNE%(UR(d@23FW=0|(^YmW(@Jdhc`CQ*nSgk% z-zHh#>U9M}o>MzeIJf-X>MG`xj+NP!sb{Kf#ceash(Ic1 z3fwf3uhDW!*ozFp7fAUwl2C?-R*btI-utvNAU@qgSWqIc7%d_hk^GG^&9KG@Noqe;#A2jtYexTy$4i3KJ~6>R~3W# zml@8f?-E#jh~rB`wFH-LfhJ#y=o&=1E`LXdRLL`M^0__0{*iq)t(YMUM9z z8V^h5;QN~ywSB)+WK`0IC4!xzU% z2%`gR8zH*C)kP%81lLaNsmh86kQcjYBOhCfji#BPU)V$w-PcJwu?vxC^BaQI+`{3y zIjUzxVRUtv3(T3NCdNak7Am>b9CCB1`DIN%u&-uSfxhkC>A8~s+vi=B%QPM$DU8TI z&Pqftv(R5VNG#3gefS6PuV96@Jnu+?2mk=C^IruknhO6eSV8-5f)#0V8%uK=)Bi!N z;ssq}`ZprD{t>I-uG?Ub-*5Z<26RyvAk<2)gBXp^A@XC4=!>uoYrvbP zagsoX_^meEakgvl@_0Wyjq}K) zyuC80SBFFr5~AdYJC5*&Ar|79S(n4OMX{L z6qiM08}Q^$;>sd3h)L=$l#s~-@tX)R!F-V?1ZNL)I+8^!4Ur+8#IXhLeK5jAcsMj) zR4m+3WXE?e771__UJnu4E z0s+mG$UO~>8GbgW@JA{cD3wu+M<~HwKqy8-zau+n3V6ImC!OB@@bUU`i+fdZJ_lD1=$(OzuNWB5i$;BwbwDQCV6nr$m)< zqfZ-!b=q~O1F{kliBi6Sl8%eQh-w&Z6IBpF*BX>w)flRLf-?HAgief=uoeO9KX<8$ zUC$``>wHIignP)YW*5*I1Iep!ftjO-a9_~-&3-M~Va*dHONILhFrj7zLFU*{&4X@# zR`HCUA1z;23U9g)GZM28=(9ckS-!C{WB~I&%#Fbl$GqG#Tl{P=T1PO1kqf{^pyZ{K z9b*0iDfS)K=!hso5k)$iGsy}F`0AnUU=ozhZlf9N0sNplU1F`n$6&B>WB|!#B6pA{ zu(^<@*@70remJ>m-H?~>sa()^?jRXJ)UJRqC+w684BA8lM&-4g$gl=s6<@bm8Q{-r zWCzLuE=+s00XB2+?Aq_K<8gtywL z1PB#&c3;O8@iru*AfOv;pAQ*J8h&;^Mf-G zNdVqK7}zCgV@oUvG*hbQ`)*6taG>My%>={U`@lJX6p9uh(LiEqOEjh$g!9t&D&UQE z0u!JocGunDiL%8|IfvZXQRR}Zpq^!y{FtTSGQ^@W3E>bGWoPgxn#L%I2q3}+1^;Qo zNOT#oxY!wUXsF@BvP_QM&2HR}>^~x~1{im!TgzX^%?J)6V-{iAfs>n?6BAoYc51q& zKq+3_DSrF$CrJVNjGT+s;R3Mm1T|Cpf~gTo5q7VW@SDR){h^32#sZoE=bgNycET>2 zMH!B`yivPV-9906c5Bc^K#;$J46D8LBmzoP`=FJPQ6{NFdX}v!);b|Q+Rs3aG6l42 zHuP3kJ4bQ9HQ=4)0iqVx2%z{g00LtpZVq5fiuquoSpY#}zAq z=*X8iK!Oi>AG~KLTLGF>9Zn>te@dgWqz{pI`M5qm1{jvgn2-TDxs?Z>oCpcs5iHFF zu#9m7%h4BAv5d7F!faD5ZF=+Xn}&fh{ioF|sqUOyQftGyoP*X>g1bV~oamsR66cUg zB4#QefqgjGn)rtJqB-77ZpiDy(HNh1Ls6kwt6_#?|wd^~X3uYHh}}?{j~jSgtdk zDq~%%N5WW{jb22U3guIqOi8@jY7kY{8)7RD4kfiV7#PM=u|@IkCc`eaw7VLNHC3ph zTe@6b*krKKVy{$Z3LVwidst7SJvdxuKCz716tcq?gl&N=#zNqM>&bDnG%yfY@69kD z0>_Pji+D;oR-?_^3+q9s8_NLbU77=Dhal?%%>0OBWP?`9HaMh5r*}dOTbH01)AzR- zbfY?=%V3i|=&Imy7pEBuX)J_^b$~4FQ9E^Ul=KL^!!sb*_iwXhv6PatpfW6HJv&Ie97lfcCiJMuO==)u4TW1oWb4jNm_DHJ=EyiAr%+gwe zaLjb3PV}UDSsQl~mZ5%9C(+@q-)W##lfOnxL!2DN5My1Xszu)EAC`*!sI6R?f}*BI zjYxbeQr!d$-kYvUi6u}D(#R%XLAwH#Tw#zbU%s*OOO49W|0Pek`GdrBp3BQr9Oz#Efw(VaMdfxeT;i zX@>W;wsHP2&E~3_M$1l^c5O?+w;~~DPG%D56S1Ln$6B-LgRt|=7yV!-0TsbTRZ8OA zvKv(DSB3bcDrkHr&*luf+yj9jn_TKQzD)g*OPO;o>NHKx)Lqg8 zXO~A4N&z_-n1RoE`kG;0Oy+p};~Vv0wRpp%g*J~Q0UYk+Vkp*hQcWo5uY8ab3qJYtw$I$Ar4iSt+H?vC-7` zAdb7(1)N=aQ7k>(3_ZvGA^jz^Q8RDI?K*rFf)G#QoS310_U zG$p@3TBWgL7gwGy)t9=HNqd4vf-gq=DV*hFPauVF-*i5fgbU^jz8INJYJc_Yw4MI; zKFmw#Xkgk7fLy;LjX<}s z7>O2mI#}M!SvF3RHp=9112%A%({_#P!SfsdSg+onW3{01o{an9P%Lxj0+gHzLftr} z3NfgXIf2f%#(`Nlpgv6CZk(Z6vnDa+1w3gxd>9fbwecwvDXaU|1jO8EcV9&Lj%xwm z=|B6&L}{e2Vu0~+)CdS+h!gdbalzv%{IwlhD4g^?fWm5kJViK3Du)LOeo#DSb4MV5SQ#@%#%k4fSkl2D63$0|$?q)1w@YS|geS9@=kLB4=qXVQ zEjtsK@YEU4%X0Lj%J}-muM|eqy`=HC(Oh_N4);`4Wh$-YA5_rWn#;*BN2IdN^T3vm z1!>Wq;pfLxwzI*cgCOHLYcp%VsY16ezgsJgUXL}|cEnhkkFu-II4kzbsJyyrtU9Z@ zYzBLNOxSoLm856ZDz7;oLt~@6fg)R8b{TraR&80Px;^Xbrg(N9<7Q8E?@D!Fg7WW5 zb?q7+5O(q|fEe`_Z^Oke{3+h%%C%2fdjBHIF>mBt3iK0By|sP#&@1-ZIzYoNWD4r- zq~t$IaP~%$*QmMywf0_EE@=6zLT)+;)x%@E?Z)|W`nm|avumcPtS`3sEk6N#E)#iv zO|@E|wnK=ZO`P!It-M@Ies!_%I|_x8zFJRKX;0h^dEQ&c(~~Ir(QRA%s#9~YqXs(gX(u>teu+*^aqlI+^!Pf z43eX8l$U`?3JB5N*`Zj4R^NBbkFAZWi7|pyhV%+U2-?(~SFau>0TF8ppQjE28v(8H zS@L3{n<79327j^A>{s3NB`{@QO(DfEU(mQKxu+^$o*9Locvj*lpn|PFM!)6%>_~De zF_h^7akgVRU4KJDs&`r&>ZEPZLLzMhf4LYfQ&o+!aTXA-4t*S>)+}R}+ekO=S-3)0 zK`MGe_7Km?SFRqWNhC|U1&~RzTm?>wTiY= zs=+UyCY6m}+(gJ3Mj&QZ*CP?h^t?_GP&dnc(BIS!uJ;Oqyg9owa^QNQkEJKGU6+V0 zmHRILIl~z_K>)1tX43oQ%4VldnFPno<+eFx^s$Nj(70CE#VFO`Z_;Fjw^CIyLm*i#iU6J6e{hslgKj|v%C=V4+#_})=SzNB} zs+h2#p%<;dJQC5?{iDcSmV@@V0ab{&yye!{yyR9ibIygg`Bg1FX*Tx&d+1KOhgJ=q zAU@{+mQ?u&prxCWs}p}Y$=L3}>+cIcwwudKe8@qqIN{qXxB(r|d)z0xxUilpS=A5$ zkQUUF_fjf7C3yLq`<^CtB?L=YtyvPorFKMEgy@hp%jM1>WL8(9pxx2Z{%d1an1U>g zpR|NplvmlBzl~VFT=kiOntk3m$g0AWntX`X4eE(xhMGU*<;s0?s}=09E#1~p-#AAF zT67$7rErQz`a=Wmx@^46Rh7n;;9Tm2CU=OKjoCgH9-@pxH(g_pvwGHd36E~j?MeFw z&kO5I#fmhgQ~PDM$w9AEvO)W%c(X@Qsfehi4C%&@wNm&uKj~idwpIFd05*Cj@Xd-% z3qbQ88W7QYb$Uk94&dPV6f#@sEelt~XvjKZN#gayemHSGM|I>x>%Del1aK}N7IfPQQ&-26|;J;Naj|D}i03-l_7uUa4E@ z@bB=y+7>|p0RN9&@&6$Hr(yA5t#;@D|ApA%FZtgP|2O;MKj43@i~qUYzu}vm77sUFvGzh@|p636A&OuFSW*hu>aN><)uKu{_Yj%KgYs9B$RUez59OvA;>2P literal 0 HcmV?d00001 diff --git a/Linking.py b/Linking.py index 2527631..a3d4e5d 100644 --- a/Linking.py +++ b/Linking.py @@ -3,22 +3,6 @@ from bpy.app.handlers import persistent -# DONT USE THIS!!! -# class LL_OT_LoadLightEditor(bpy.types.Operator): -# """Load the original Light Editor UI""" -# bl_idname = "light_link.load_light_editor" -# bl_label = "Load Light Editor" - -# def execute(self, context): -# import importlib -# from . import lightEditor # Adjust the import as needed -# importlib.reload(lightEditor) -# # Unregister the current Linking panel so the Light Editor can be shown. -# bpy.utils.unregister_class(LL_PT_Panel) -# lightEditor.register() -# return {'FINISHED'} - - # ------------------------------------------------------------------- # Helper: Get Selected Collections from the Outliner @@ -377,6 +361,18 @@ def execute(self, context): self.report({'WARNING'}, "No lights selected") return {'CANCELLED'} + # Gather selected meshes and meshes from selected collections + selected_meshes = [item.obj for item in scene.ll_mesh_items if item.selected and item.obj] + collection_meshes = [] + for item in scene.ll_collection_items: + if item.selected and item.coll: + for obj in item.coll.all_objects: + if obj.type == 'MESH': + collection_meshes.append(obj) + + # Combine meshes uniquely by name + all_meshes = {obj.name: obj for obj in (selected_meshes + collection_meshes)}.values() + total_removed = 0 for light in selected_lights: group_name = light.get("light_linking_receiver_collection") @@ -387,7 +383,7 @@ def execute(self, context): continue removed = 0 for obj in list(linking_group.objects): - if obj.name in [m_obj.name for m_obj in scene.ll_mesh_items if m_obj.selected]: + if obj.name in [m_obj.name for m_obj in all_meshes]: linking_group.objects.unlink(obj) removed += 1 total_removed += removed @@ -483,6 +479,18 @@ def execute(self, context): self.report({'WARNING'}, "No lights selected") return {'CANCELLED'} + # Gather selected meshes and meshes from selected collections + selected_meshes = [item.obj for item in scene.ll_mesh_items if item.selected and item.obj] + collection_meshes = [] + for item in scene.ll_collection_items: + if item.selected and item.coll: + for obj in item.coll.all_objects: + if obj.type == 'MESH': + collection_meshes.append(obj) + + # Combine meshes uniquely by name + all_meshes = {obj.name: obj for obj in (selected_meshes + collection_meshes)}.values() + total_removed = 0 for light in selected_lights: group_name = light.get("shadow_linking_blocker_collection") @@ -497,7 +505,7 @@ def execute(self, context): removed = 0 for obj in list(linking_group.objects): - if obj.name in [m_obj.name for m_obj in scene.ll_mesh_items if m_obj.selected]: + if obj.name in [m_obj.name for m_obj in all_meshes]: try: linking_group.objects.unlink(obj) removed += 1 @@ -548,7 +556,6 @@ class LL_PT_Panel(bpy.types.Panel): bl_space_type = "VIEW_3D" bl_region_type = "UI" bl_category = "Light Editor" - bl_options = {'DEFAULT_CLOSED'} def draw(self, context): layout = self.layout diff --git a/icons/select_false.png b/icons/select_false.png deleted file mode 100644 index e8d2524788c7c27d76e652a7902d0002b7c3e035..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 563 zcmV-30?hr1P)A^nB6@J1rKN53GnP8>L^6UK@3)FbH^ZOXWY;9ec2|Jr2;>d1SM2XcBPF?UE5 zoc{(4<*KG6jmOaL!kUOgG?Q{&nlmQ{tYNst_znoI$_q%c)ad7BM~y_Zc0i3EWP>#~ zl7RQ#s5l41g>3q+1wWK3CwB~NM`$k>Wd9&!tUFTR8x6=xJX-#31E3BoDj}?L5?W zfVihs+}%*z5_eo>!jPK3YHL~c@VU|0>R$i{xRlwnp>Z|*sqm(BTh)SqcHbf)i&xXy zx)_0o1sfz)4~nXj^JMe*QyklV%@X3cfvQix%rAaUF2>(l{SDdF0TsSc0b?rOiCc1b zl)6xQAjYpU=-4u;9OLzSsYo4h?9kQc_2%*|zyKrWp4r<~^7jA$002ovPDHLkV1mB6 B0UH1S diff --git a/icons/select_true.png b/icons/select_true.png deleted file mode 100644 index daa3cf512a93a659adf3b1e445ab5d4b9946ff6c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 429 zcmV;e0aE^nP)Af1otTuRC@d$O4l=op$q7d9gh8+6c|QwbzORJJ}j(0d2`=c}Ntk*3&H6=aHh z-xT4`jRBB2d!k>0G*_F%)QP}`B^Gf9-H;sUAR?)60H@k~{%9&*03hc_3}uZ>sqYl* zLiYi|%V!t>NJHf)3Z+RE2nPU;+5mX(-&%Ao7+Iq}u!deOIIs9EvqnV36AOe~s{W+3 zPD3sn)(K=FFRy0W$1xA2z9A=M)Q0cwg^f?P!_!iB#u0R&Fp*#nD$+R see @persistent @@ -1081,13 +1026,8 @@ def register(): # bpy.app.handlers.depsgraph_update_post.append(LE_check_lights_enabled) bpy.app.handlers.load_post.append(LE_check_lights_enabled) - - def unregister(): - global custom_icons - bpy.utils.previews.remove(custom_icons) - #removove handler post load > see @persistent bpy.app.handlers.load_post.remove(LE_clear_handler) bpy.app.handlers.load_post.remove(LE_check_lights_enabled) @@ -1095,7 +1035,8 @@ def unregister(): # Unregister scene properties del bpy.types.Scene.current_active_light del bpy.types.Scene.current_exclusive_group - + del bpy.types.Scene.selected_render_layer + # Unregister other classes and properties for cls in reversed(classes): bpy.utils.unregister_class(cls) From 3447a29481dea156bf9b865bced8882c78bb451a Mon Sep 17 00:00:00 2001 From: schroef Date: Tue, 18 Feb 2025 18:36:31 -0400 Subject: [PATCH 3/6] update from stream --- LightEditor.py | 1019 +++++++++++++++++++++++++++++++----------------- 1 file changed, 663 insertions(+), 356 deletions(-) diff --git a/LightEditor.py b/LightEditor.py index dfe09f4..c2a99c8 100644 --- a/LightEditor.py +++ b/LightEditor.py @@ -15,54 +15,93 @@ current_active_light = None current_exclusive_group = None -# Global dictionaries to track states -group_checkbox_1_state = {} # Used for first button (toggle group on/off) +# Dictionaries to store states +group_checkbox_1_state = {} # First button on/off per group_key (ON by default) group_lights_original_state = {} # Stores original light states for each group +group_checkbox_2_state = {} # Second button on/off per group_key (OFF by default) +other_groups_original_state = {} # Stores other groups' states group_collapse_dict = {} # Whether each group is collapsed -collections_with_lights = {} # Tracks collections that contain lights -# New globals for the exclusive (second) button -group_checkbox_2_state = {} # Exclusive button state per group (False by default) -other_groups_original_state = {} # Stores the other groups' light states +# ------------------------------------------------------------------------- +# Add a new operator for deleting a light group +# ------------------------------------------------------------------------- +class LIGHT_OT_DeleteLightGroup(bpy.types.Operator): + """Delete the selected light group.""" + bl_idname = "light_editor.delete_light_group" + bl_label = "Delete Light Group" + + def execute(self, context): + scene = context.scene + selected_group = scene.light_editor_delete_group + + if selected_group == "NoGroup": + self.report({'WARNING'}, "No group selected.") + return {'CANCELLED'} + + # Remove the group from all lights (use context.view_layer.objects) + for obj in context.view_layer.objects: + if obj.type == 'LIGHT' and getattr(obj, "custom_lightgroup", "") == selected_group: + obj.custom_lightgroup = "" + obj.custom_lightgroup_menu = "NoGroup" + + # Remove the group from the scene's custom_lightgroups_list + if hasattr(scene, "custom_lightgroups_list") and scene.custom_lightgroups_list: + groups = [g.strip() for g in scene.custom_lightgroups_list.split(",") if g.strip()] + if selected_group in groups: + groups.remove(selected_group) + scene.custom_lightgroups_list = ", ".join(groups) + + self.report({'INFO'}, f"Deleted light group '{selected_group}'") + return {'FINISHED'} # ------------------------------------------------------------------------- -# Render layer functions +# 1) Functions for custom light group handling # ------------------------------------------------------------------------- -def update_render_layer(self, context): - selected = self.selected_render_layer - for vl in context.scene.view_layers: - if vl.name == selected: - context.window.view_layer = vl - break +def get_light_groups(context): + """ + Loop through all objects, find lights, and collect them by their custom light group. + If a light already has an old "lightgroup" value, it is used. + Also include any groups stored in the scene property. + Returns a dictionary mapping group names to lists of light object names. + """ + groups = {} + for obj in context.view_layer.objects: + if obj.type == 'LIGHT': + # Try the new property; if empty, fallback to old property. + group_val = getattr(obj, "lightgroup", "") + if not group_val: + group_val = getattr(obj, "lightgroup", "") + if group_val: + groups.setdefault(group_val, []).append(obj.name) + # Get any groups created via the operator (even if no light has that assignment) + scene = context.scene + if hasattr(scene, "custom_lightgroups_list") and scene.custom_lightgroups_list: + extra_groups = [g.strip() for g in scene.custom_lightgroups_list.split(",") if g.strip()] + for g in extra_groups: + if g not in groups: + groups[g] = [] # no lights assigned yet + return groups -def get_render_layer_items(self, context): +def get_light_group_items(self, context): + """Return a list of custom light group items for the EnumProperty.""" + items = [("NoGroup", "No Group", "")] + groups = get_light_groups(context) # Pass context to get_light_groups + for group_name in groups.keys(): + if group_name != "NoGroup": + items.append((group_name, group_name, "")) + return items + +def update_lightgroup_menu(self, context): + """When the menu selection changes, copy the value into our custom_lightgroup property.""" + self.custom_lightgroup = self.custom_lightgroup_menu + +def view_layer_items(self, context): + """Return a list of view layer items for the EnumProperty.""" items = [] for view_layer in context.scene.view_layers: items.append((view_layer.name, view_layer.name, "")) return items -# ------------------------------------------------------------------------- -# Collection functions -# ------------------------------------------------------------------------- -def gather_layer_collections(parent_lc, result): - """Recursively gather all LayerCollections in the parent_lc hierarchy.""" - result.append(parent_lc) - for child in parent_lc.children: - gather_layer_collections(child, result) - -def get_layer_collection_by_name(layer_collection, coll_name): - """Recursively search for a LayerCollection with the given name.""" - if layer_collection.collection.name == coll_name: - return layer_collection - for child in layer_collection.children: - found = get_layer_collection_by_name(child, coll_name) - if found: - return found - return None - -# ------------------------------------------------------------------------- -# Light update functions -# ------------------------------------------------------------------------- def update_light_enabled(self, context): self.hide_viewport = not self.light_enabled self.hide_render = not self.light_enabled @@ -73,6 +112,7 @@ def update_light_turn_off_others(self, context): if scene.current_active_light and scene.current_active_light != self: scene.current_active_light.light_turn_off_others = False scene.current_active_light = self + # Use objects from the active view layer for obj in context.view_layer.objects: if obj.type == 'LIGHT' and obj.name != self.name: if 'prev_light_enabled' not in obj: @@ -87,35 +127,35 @@ def update_light_turn_off_others(self, context): obj.light_enabled = obj['prev_light_enabled'] del obj['prev_light_enabled'] -def get_all_collections(obj): - def _get_collections_recursive(collection, path=None): - if path is None: - path = [] - path.append(collection.name) - yield path[:] - for child in collection.children: - yield from _get_collections_recursive(child, path) - path.pop() - all_collections = set() - for collection in obj.users_collection: - for path in _get_collections_recursive(collection): - all_collections.add(" > ".join(path)) - return sorted(all_collections) - -# ------------------------------------------------------------------------- -# Mutually exclusive grouping toggles -# ------------------------------------------------------------------------- +# --- MUTUALLY EXCLUSIVE GROUPING TOGGLES --- def update_group_by_kind(self, context): if self.light_editor_kind_alpha: self.light_editor_group_by_collection = False + self.light_editor_light_group = False def update_group_by_collection(self, context): if self.light_editor_group_by_collection: self.light_editor_kind_alpha = False + self.light_editor_light_group = False -# ------------------------------------------------------------------------- -# Device functions (for cycles) -# ------------------------------------------------------------------------- +def update_light_group(self, context): + if self.light_editor_light_group: + self.light_editor_kind_alpha = False + self.light_editor_group_by_collection = False + + # Collapse all expanded lights when switching to "By Light Groups" mode + for obj in context.view_layer.objects: + if obj.type == 'LIGHT': + obj.light_expanded = False + +def update_light_types(self, context): + if self == 'group': + # Collapse all expanded lights when switching to "By Light Groups" mode + for obj in context.view_layer.objects: + if obj.type == 'LIGHT': + obj.light_expanded = False + +# Used for MacOS def get_device_type(context): return context.preferences.addons['cycles'].preferences.compute_device_type @@ -124,9 +164,11 @@ def backend_has_active_gpu(context): def use_metal(context): cscene = context.scene.cycles + return (get_device_type(context) == 'METAL' and cscene.device == 'GPU' and backend_has_active_gpu(context)) def use_mnee(context): + # The MNEE kernel doesn't compile on macOS < 13. if use_metal(context): import platform version, _, _ = platform.mac_ver() @@ -136,15 +178,21 @@ def use_mnee(context): return True # ------------------------------------------------------------------------- -# Extra parameters drawing function (unchanged) +# 2) Extra parameters drawing function (unchanged) # ------------------------------------------------------------------------- def draw_extra_params(self, box, obj, light): + # Draw the default light properties UI in the provided box if light and isinstance(light, bpy.types.Light): + # Copy the layout drawing logic from DATA_PT_light layout = box + + # Display the 4 light type buttons in a single row row = layout.row() row.prop(light, "type", expand=True) + col = layout.column() col.separator() + if bpy.context.engine == 'CYCLES': clamp = light.cycles if light.type in {'POINT', 'SPOT'}: @@ -155,23 +203,29 @@ def draw_extra_params(self, box, obj, light): elif light.type == 'AREA': col.prop(light, "shape", text="Shape") sub = col.column(align=True) + if light.shape in {'SQUARE', 'DISK'}: sub.prop(light, "size") elif light.shape in {'RECTANGLE', 'ELLIPSE'}: sub.prop(light, "size", text="Size X") sub.prop(light, "size_y", text="Y") + if not (light.type == 'AREA' and clamp.is_portal): col.separator() sub = col.column() sub.prop(clamp, "max_bounces") + sub = col.column(align=True) sub.active = not (light.type == 'AREA' and clamp.is_portal) + sub.prop(light, "use_shadow", text="Cast Shadow") sub.prop(clamp, "use_multiple_importance_sampling", text="Multiple Importance") if use_mnee(bpy.context): sub.prop(clamp, "is_caustics_light", text="Shadow Caustics") + if light.type == 'AREA': col.prop(clamp, "is_portal", text="Portal") + if light.type == 'SPOT': col.separator() row = col.row(align=True) @@ -180,12 +234,15 @@ def draw_extra_params(self, box, obj, light): col.prop(light, "spot_size", text="Spot Size") col.prop(light, "spot_blend", text="Blend", slider=True) col.prop(light, "show_cone") + elif light.type == 'AREA': col.separator() row = col.row(align=True) row.alignment = 'CENTER' row.label(text="Beam Shape") col.prop(light, "spread", text="Spread") + + # EEVEE if ((bpy.context.engine == 'BLENDER_EEVEE') or (bpy.context.engine == 'BLENDER_EEVEE_NEXT')): col.separator() if light.type in {'POINT', 'SPOT'}: @@ -195,12 +252,15 @@ def draw_extra_params(self, box, obj, light): col.prop(light, "angle") elif light.type == 'AREA': col.prop(light, "shape") + sub = col.column(align=True) + if light.shape in {'SQUARE', 'DISK'}: sub.prop(light, "size") elif light.shape in {'RECTANGLE', 'ELLIPSE'}: sub.prop(light, "size", text="Size X") sub.prop(light, "size_y", text="Y") + if bpy.context.engine == 'BLENDER_EEVEE_NEXT': col.separator() col.prop(light, "use_shadow", text="Cast Shadow") @@ -208,7 +268,8 @@ def draw_extra_params(self, box, obj, light): col.prop(light, "shadow_jitter_overblur", text="Overblur") col.prop(light, "shadow_filter_radius", text="Radius") col.prop(light, "shadow_maximum_resolution", text="Resolution Limit") - if light and light.type == 'SPOT': + + if (light and light.type == 'SPOT'): col.separator() row = col.row(align=True) row.alignment = 'CENTER' @@ -216,25 +277,80 @@ def draw_extra_params(self, box, obj, light): col.prop(light, "spot_size", text="Size") col.prop(light, "spot_blend", text="Blend", slider=True) col.prop(light, "show_cone") + col.separator() col.prop(light, "diffuse_factor", text="Diffuse") col.prop(light, "specular_factor", text="Specular") col.prop(light, "volume_factor", text="Volume", text_ctxt=i18n_contexts.id_id) - if light.type != 'SUN': + + if (light and light.type != 'SUN'): col.separator() sub = col.column() sub.prop(light, "use_custom_distance", text="Custom Distance") sub.active = light.use_custom_distance sub.prop(light, "cutoff_distance", text="Distance") +# ------------------------------------------------------------------------- +# Render layer menu +# ------------------------------------------------------------------------- + +def get_render_layer_items(self, context): + """Return a list of render layer items for the EnumProperty.""" + items = [] + for view_layer in context.scene.view_layers: + items.append((view_layer.name, view_layer.name, "")) + return items + +def update_render_layer(self, context): + selected = self.selected_render_layer + # Iterate over the scene’s view layers: + for vl in context.scene.view_layers: + if vl.name == selected: + context.window.view_layer = vl + break + + +# ------------------------------------------------------------------------- +# 3) Operator: Add Lightgroup +# This operator now works even if no light is selected. +# When executed it assigns the new group name to the active light (if one exists) +# or adds the group name to the scene's custom_lightgroups_list. +# ------------------------------------------------------------------------- +class LIGHT_OT_CreateNewLightgroup(bpy.types.Operator): + """Add a new lightgroup and assign it (if a light is selected) or store it for later use.""" + bl_idname = "light_editor.create_new_lightgroup" + bl_label = "Add Lightgroup" + + def execute(self, context): + scene = context.scene + new_group = scene.create_new_lightgroup.strip() + if not new_group: + self.report({'WARNING'}, "No group name entered.") + return {'CANCELLED'} + active_obj = context.active_object + if active_obj and active_obj.type == 'LIGHT': + active_obj.custom_lightgroup = new_group + active_obj.custom_lightgroup_menu = new_group + self.report({'INFO'}, f"Assigned group '{new_group}' to {active_obj.name}") + else: + # No active light: add the group name to the scene list (if not already present) + current_list = scene.custom_lightgroups_list + groups = [g.strip() for g in current_list.split(",") if g.strip()] if current_list else [] + if new_group not in groups: + groups.append(new_group) + scene.custom_lightgroups_list = ", ".join(groups) + self.report({'INFO'}, f"Created light group '{new_group}'") + scene.create_new_lightgroup = "" + return {'FINISHED'} # ------------------------------------------------------------------------- -# Operators +# 4) Operators for group toggling (object access updated) # ------------------------------------------------------------------------- class LIGHT_OT_ToggleGroup(bpy.types.Operator): """Collapse or expand a group header.""" bl_idname = "light_editor.toggle_group" bl_label = "Toggle Group" - group_key: bpy.props.StringProperty() + group_key: StringProperty() + def execute(self, context): current = group_collapse_dict.get(self.group_key, False) group_collapse_dict[self.group_key] = not current @@ -243,42 +359,12 @@ def execute(self, context): area.tag_redraw() return {'FINISHED'} -class LIGHT_OT_ToggleCollection(bpy.types.Operator): - """Toggle the exclusion state of a collection (and its sub-collections).""" - bl_idname = "light_editor.toggle_collection" - bl_label = "Toggle Collection Exclusion" - group_key: bpy.props.StringProperty() - def execute(self, context): - global collections_with_lights - if self.group_key.startswith("coll_"): - coll_name = self.group_key[5:] - collection = bpy.data.collections.get(coll_name) - if not collection: - return {'CANCELLED'} - def toggle_exclusion_recursive(layer_coll, exclude): - layer_coll.exclude = exclude - for child in layer_coll.children: - toggle_exclusion_recursive(child, exclude) - layer_collection = get_layer_collection_by_name(context.view_layer.layer_collection, coll_name) - if layer_collection: - current_excluded = layer_collection.exclude - toggle_exclusion_recursive(layer_collection, not current_excluded) - def has_lights(coll): - return any(obj.type == 'LIGHT' for obj in coll.all_objects) - if has_lights(collection): - collections_with_lights[coll_name] = True - else: - collections_with_lights.pop(coll_name, None) - for area in context.screen.areas: - if area.type == 'VIEW_3D': - area.tag_redraw() - return {'FINISHED'} +class LIGHT_OT_ToggleGroupAllOff(bpy.types.Operator): + """Toggle all lights in the group off or restore them.""" + bl_idname = "light_editor.toggle_group_all_off" + bl_label = "Toggle Group All Off" + group_key: StringProperty() -class LIGHT_OT_ToggleKind(bpy.types.Operator): - """Toggle all lights of a specific kind.""" - bl_idname = "light_editor.toggle_kind" - bl_label = "Toggle Kind Visibility" - group_key: bpy.props.StringProperty() def execute(self, context): global group_checkbox_1_state, group_lights_original_state is_on = group_checkbox_1_state.get(self.group_key, True) @@ -286,161 +372,227 @@ def execute(self, context): if is_on: original_states = {} for obj in group_objs: - if obj.type == 'LIGHT': - original_states[obj.name] = obj.light_enabled - obj.light_enabled = False + original_states[obj.name] = obj.light_enabled group_lights_original_state[self.group_key] = original_states + for obj in group_objs: + obj.light_enabled = False + group_checkbox_1_state[self.group_key] = False else: original_states = group_lights_original_state.get(self.group_key, {}) for obj in group_objs: - if obj.type == 'LIGHT': - obj.light_enabled = original_states.get(obj.name, True) + obj.light_enabled = original_states.get(obj.name, True) if self.group_key in group_lights_original_state: del group_lights_original_state[self.group_key] - group_checkbox_1_state[self.group_key] = not is_on + group_checkbox_1_state[self.group_key] = True for area in context.screen.areas: if area.type == 'VIEW_3D': area.tag_redraw() return {'FINISHED'} + def _get_group_objects(self, context, group_key): - filter_pattern = context.scene.light_editor_filter.lower() - all_lights = [obj for obj in context.view_layer.objects if obj.type == 'LIGHT'] + scene = context.scene + filter_pattern = scene.light_editor_filter.lower() if filter_pattern: - all_lights = [obj for obj in all_lights if re.search(filter_pattern, obj.name, re.I)] - kind = group_key[5:] - return [obj for obj in all_lights if obj.data.type == kind] + # all_lights = [obj for obj in context.view_layer.objects + # if obj.type == 'LIGHT' and fnmatch.fnmatch(obj.name.lower(), filter_pattern)] + + all_lights = [obj for obj in context.view_layer.objects + if obj.type == 'LIGHT' and re.search(filter_pattern, obj.name, re.I)] + + # for fn in sorted(files): + # if not fn.lower().startswith("._"): + # if fn.lower().endswith(".hdr") or fn.lower().endswith(".exr") or fn.lower().endswith(".jpg") or fn.lower().endswith(".png"): + # if not scn.easyhdr_filter or re.search(scn.easyhdr_filter, fn, re.I): + # hdris.append(os.path.join(root, fn).replace(dir, '')) + # no_match = False + else: + all_lights = [obj for obj in context.view_layer.objects if obj.type == 'LIGHT'] + if scene.filter_light_types == 'COLLECTION' and group_key.startswith("coll_"): + coll_name = group_key[5:] + return [obj for obj in all_lights + if (obj.users_collection and obj.users_collection[0].name == coll_name) + or (not obj.users_collection and coll_name == "No Collection")] + if scene.filter_light_types == 'KIND' and group_key.startswith("kind_"): + kind = group_key[5:] + return [obj for obj in all_lights if obj.data.type == kind] + if scene.filter_light_types == 'GROUP' and group_key.startswith("group_"): + group_name = group_key[6:] + return [obj for obj in all_lights if getattr(obj, "custom_lightgroup", "") == group_name] + return [] class LIGHT_OT_ToggleGroupExclusive(bpy.types.Operator): """ Toggle exclusive mode for a group. - When turned on, lights in all other groups are turned off. - When turned off, the previous states of the other groups are restored. + When turned on, lights in other groups (including "Not Assigned") are turned off. + When turned off, lights in other groups are restored to their previous states. """ bl_idname = "light_editor.toggle_group_exclusive" bl_label = "Toggle Group Exclusive" - group_key: bpy.props.StringProperty() + group_key: StringProperty() + def execute(self, context): global current_exclusive_group, group_checkbox_2_state, other_groups_original_state + + # Check if the current group is already in exclusive mode is_on = group_checkbox_2_state.get(self.group_key, False) + if not is_on: + # If another group is already in exclusive mode, turn it off first if current_exclusive_group and current_exclusive_group != self.group_key: - saved = other_groups_original_state.get(current_exclusive_group, {}) - for gk, states in saved.items(): - objs = self._get_group_objects(context, gk) - for obj in objs: - obj.light_enabled = states.get(obj.name, True) group_checkbox_2_state[current_exclusive_group] = False + saved_dict = other_groups_original_state.get(current_exclusive_group, {}) + for gk, light_dict in saved_dict.items(): + grp_objs = self._get_group_objects(context, gk) + for obj in grp_objs: + obj.light_enabled = light_dict.get(obj.name, True) if current_exclusive_group in other_groups_original_state: del other_groups_original_state[current_exclusive_group] + + # Turn off all other groups (including "Not Assigned") and store their current states others = self._get_all_other_groups(context, self.group_key) - saved = {} + saved_dict = {} for gk in others: - objs = self._get_group_objects(context, gk) - saved[gk] = {obj.name: obj.light_enabled for obj in objs} - for obj in objs: + grp_objs = self._get_group_objects(context, gk) + saved_dict[gk] = {obj.name: obj.light_enabled for obj in grp_objs} + for obj in grp_objs: obj.light_enabled = False - other_groups_original_state[self.group_key] = saved + + # Store the saved states and mark the current group as exclusive + other_groups_original_state[self.group_key] = saved_dict group_checkbox_2_state[self.group_key] = True current_exclusive_group = self.group_key + else: - saved = other_groups_original_state.get(self.group_key, {}) - for gk, states in saved.items(): - objs = self._get_group_objects(context, gk) - for obj in objs: - obj.light_enabled = states.get(obj.name, True) + # Restore the states of all other groups (including "Not Assigned") + saved_dict = other_groups_original_state.get(self.group_key, {}) + for gk, light_dict in saved_dict.items(): + grp_objs = self._get_group_objects(context, gk) + for obj in grp_objs: + obj.light_enabled = light_dict.get(obj.name, True) + + # Clear the saved states and mark the current group as non-exclusive if self.group_key in other_groups_original_state: del other_groups_original_state[self.group_key] group_checkbox_2_state[self.group_key] = False current_exclusive_group = None + + # Redraw the UI to reflect the changes for area in context.screen.areas: if area.type == 'VIEW_3D': area.tag_redraw() + return {'FINISHED'} + def _get_all_other_groups(self, context, current_key): + """Get all group keys except the current one, including "Not Assigned".""" all_groups = self._get_all_group_keys(context) return [k for k in all_groups if k != current_key] + def _get_all_group_keys(self, context): + """Get all group keys, including "Not Assigned".""" scene = context.scene - filter_type = scene.filter_light_types - keys = set() - if filter_type == 'COLLECTION': - for obj in context.view_layer.objects: - if obj.type == 'LIGHT': - if obj.users_collection: - keys.add("coll_" + obj.users_collection[0].name) - else: - keys.add("coll_No Collection") - elif filter_type == 'KIND': - for obj in context.view_layer.objects: - if obj.type == 'LIGHT': - keys.add("kind_" + obj.data.type) - elif filter_type == 'GROUP': - for obj in context.view_layer.objects: - if obj.type == 'LIGHT': - grp = getattr(obj, "lightgroup", "") - if not grp: - grp = "Not Assigned" - keys.add("group_" + grp) - return list(keys) + filter_pattern = scene.light_editor_filter.lower() + if filter_pattern: + lights = [obj for obj in context.view_layer.objects + if obj.type == 'LIGHT' and re.search(filter_pattern, obj.name, re.I)] + else: + lights = [obj for obj in context.view_layer.objects if obj.type == 'LIGHT'] + + group_keys = set() + if scene.filter_light_types == 'COLLECTION': + for obj in lights: + if obj.users_collection: + coll_name = obj.users_collection[0].name + else: + coll_name = "No Collection" + group_keys.add(f"coll_{coll_name}") + elif scene.filter_light_types == 'KIND': + for obj in lights: + if obj.data.type in {'POINT', 'SPOT', 'SUN', 'AREA'}: + group_keys.add(f"kind_{obj.data.type}") + elif scene.filter_light_types == 'GROUP': + for obj in lights: + group_name = getattr(obj, "lightgroup", "") + if not group_name: + group_name = "Not Assigned" # Explicitly handle "Not Assigned" + group_keys.add(f"group_{group_name}") + return list(group_keys) + def _get_group_objects(self, context, group_key): + """Get all objects in the specified group, including "Not Assigned".""" scene = context.scene filter_pattern = scene.light_editor_filter.lower() - all_lights = [obj for obj in context.view_layer.objects if obj.type == 'LIGHT'] if filter_pattern: - all_lights = [obj for obj in all_lights if re.search(filter_pattern, obj.name, re.I)] + all_lights = [obj for obj in context.view_layer.objects + if obj.type == 'LIGHT' and re.search(filter_pattern, obj.name, re.I)] + else: + all_lights = [obj for obj in context.view_layer.objects if obj.type == 'LIGHT'] + if scene.filter_light_types == 'COLLECTION' and group_key.startswith("coll_"): coll_name = group_key[5:] - return [obj for obj in all_lights if (obj.users_collection and obj.users_collection[0].name == coll_name) or (not obj.users_collection and coll_name == "No Collection")] - elif scene.filter_light_types == 'KIND' and group_key.startswith("kind_"): + return [obj for obj in all_lights + if (obj.users_collection and obj.users_collection[0].name == coll_name) + or (not obj.users_collection and coll_name == "No Collection")] + if scene.filter_light_types == 'KIND' and group_key.startswith("kind_"): kind = group_key[5:] return [obj for obj in all_lights if obj.data.type == kind] - elif scene.filter_light_types == 'GROUP' and group_key.startswith("group_"): - grp_name = group_key[6:] - if grp_name == "Not Assigned": + if scene.filter_light_types == 'GROUP' and group_key.startswith("group_"): + group_name = group_key[6:] + if group_name == "Not Assigned": return [obj for obj in all_lights if not getattr(obj, "lightgroup", "")] else: - return [obj for obj in all_lights if getattr(obj, "lightgroup", "") == grp_name] + return [obj for obj in all_lights if getattr(obj, "lightgroup", "") == group_name] return [] class LIGHT_OT_ClearFilter(bpy.types.Operator): """Clear Filter Types""" bl_idname = "le.clear_light_filter" bl_label = "Clear Filter" + @classmethod - def description(cls, context, properties): - scene = context.scene - if scene.filter_light_types == 'COLLECTION': - return "Turn ON/OFF Collection" - elif scene.filter_light_types == 'KIND': - return "Turn ON/OFF All Lights of This Kind" - return "Toggle all lights in the group off or restore them" + def poll(cls, context): + return context.scene.light_editor_filter + def execute(self, context): - context.scene.light_editor_filter = "" + if context.scene.light_editor_filter: + context.scene.light_editor_filter = "" return {'FINISHED'} + class LIGHT_OT_SelectLight(bpy.types.Operator): """Selects light from UI and deselects everything else""" bl_idname = "le.select_light" bl_label = "Select Light" - name : StringProperty() + + name : StringProperty() # Name of the light to select + def execute(self, context): + # Get the view layer objects vob = context.view_layer.objects + + # Check if the light is already selected if self.name in vob: light = vob[self.name] - if light.select_get(): + if light.select_get(): # If the light is already selected + # Deselect everything bpy.ops.object.select_all(action='DESELECT') self.report({'INFO'}, f"Deselected all objects") else: + # Deselect all objects first bpy.ops.object.select_all(action='DESELECT') + # Select the specified light light.select_set(True) - vob.active = light + vob.active = light # Set the light as the active object self.report({'INFO'}, f"Selected light: {self.name}") else: self.report({'ERROR'}, f"Light '{self.name}' not found") + return {'FINISHED'} + + # ------------------------------------------------------------------------- -# Main row UI drawing function +# 4) The main row UI: use custom_lightgroup property when in Light Group mode # ------------------------------------------------------------------------- def draw_main_row(box, obj): light = obj.data @@ -448,32 +600,68 @@ def draw_main_row(box, obj): scene = bpy.context.scene row = box.row(align=True) controls_row = row.row(align=True) + + # The enabled and "turn off others" toggles remain: controls_row.prop(obj, "light_enabled", text="", icon="OUTLINER_OB_LIGHT" if obj.light_enabled else "LIGHT_DATA") - controls_row.active = (obj.light_enabled == True) + controls_row.active = obj.light_enabled == True controls_row.prop(obj, "light_turn_off_others", text="", icon="RADIOBUT_ON" if obj.light_turn_off_others else "RADIOBUT_OFF") + if not scene.filter_light_types == 'GROUP': selected_true = custom_icons["SELECT_TRUE"].icon_id selected_false = custom_icons["SELECT_FALSE"].icon_id - controls_row.operator("le.select_light", text="", - icon_value=selected_true if obj.select_get() else selected_false).name = obj.name + controls_row.operator("le.select_light", text="", + icon_value=selected_true if obj.select_get() == True else selected_false).name = obj.name + + # -- Only show the triangle if NOT in Light Group mode -- if not scene.filter_light_types == 'GROUP': controls_row.prop(obj, "light_expanded", text="", emboss=True, icon='DOWNARROW_HLT' if obj.light_expanded else 'RIGHTARROW') - col_name = row.column(align=True) - col_name.scale_x = 0.4 - col_name.prop(obj, "name", text="") - col_color = row.column(align=True) - col_color.scale_x = 0.25 - col_color.prop(light, "color", text="") - col_energy = row.column(align=True) - col_energy.scale_x = 0.35 - col_energy.prop(light, "energy", text="") + + # Apply 50-50 split only in Light Group mode + if scene.filter_light_types == 'GROUP': + split = row.split(factor=0.5) + col_name = split.column(align=True) + col_name.prop(obj, "name", text="") # Default width in Light Group mode + + col_group = split.column(align=True) + view_layer = context.view_layer + + row_group = col_group.row(align=True) + sub = row_group.column(align=True) + sub.prop_search(obj, "lightgroup", view_layer, "lightgroups", text="", results_are_suggestions=True) + + sub = row_group.column(align=True) + sub.enabled = bool(obj.lightgroup) and not any(lg.name == obj.lightgroup for lg in view_layer.lightgroups) + sub.operator("scene.view_layer_add_lightgroup", icon='ADD', text="").name = obj.lightgroup + + else: + # Original layout for other modes, with a shorter name field + col_name = row.column(align=True) + col_name.scale_x = 0.4 # Make the name field shorter + col_name.prop(obj, "name", text="") + + col_color = row.column(align=True) + col_color.scale_x = 0.25 + col_color.prop(light, "color", text="") + col_energy = row.column(align=True) + col_energy.scale_x = 0.35 + col_energy.prop(light, "energy", text="") + +# class DataButtonsPanel: +# bl_space_type = 'PROPERTIES' +# bl_region_type = 'WINDOW' +# bl_context = "data" + +# @classmethod +# def poll(cls, context): +# engine = context.engine +# return context.light and (engine in cls.COMPAT_ENGINES) # ------------------------------------------------------------------------- -# Main panel +# 5) The main panel # ------------------------------------------------------------------------- class LIGHT_PT_editor(bpy.types.Panel): """Panel to view/edit lights with grouping, filtering, and per‐group toggles.""" @@ -482,192 +670,269 @@ class LIGHT_PT_editor(bpy.types.Panel): bl_space_type = "VIEW_3D" bl_region_type = "UI" bl_category = "Light Editor" + def draw(self, context): layout = self.layout scene = context.scene + layout.row().prop(scene, "filter_light_types", expand=True) layout.use_property_split = True - layout.use_property_decorate = False + layout.use_property_decorate = False # No animation. + + + # Top toggles with new labels. + # row = layout.row(align=True) + # row.prop(scene, "light_editor_kind_alpha", text="By Kind") + # row.prop(scene, "light_editor_group_by_collection", text="By Collections") + # row.prop(scene, "light_editor_light_group", text="By Light Groups") + + row = layout.row(align=True) row.prop(scene, "light_editor_filter", text="", icon="VIEWZOOM") row.operator("le.clear_light_filter", text="", icon='PANEL_CLOSE') - # Render layer dropdown in COLLECTION mode: + # Only display the render layer dropdown when in Collection mode. if scene.filter_light_types == 'COLLECTION': row = layout.row() row.prop(scene, "selected_render_layer", text="Render Layer") - - # COLLECTION mode: group by collection + + # Only show these rows if in Light Group mode. + if scene.filter_light_types == 'GROUP': + # # Row for adding a new light group. + # row = layout.row(align=True) + # row.prop(scene, "create_new_lightgroup", text="") + # row.operator("light_editor.create_new_lightgroup", text="Add Lightgroup") + + # # Row for selecting and deleting a light group. + # row = layout.row(align=True) + # row.prop(scene, "light_editor_delete_group", text="") + # row.operator("light_editor.delete_light_group", text="Delete Light Group") + + # copy from passes panel + view_layer = context.view_layer + + # layout.label(text="Light Groups") + # layout.alignment = 'CENTER' + row = layout.row() + col = row.column() + col.template_list("UI_UL_list", "lightgroups", view_layer, + "lightgroups", view_layer, "active_lightgroup_index", rows=3) + + col = row.column() + sub = col.column(align=True) + sub.operator("scene.view_layer_add_lightgroup", icon='ADD', text="") + sub.operator("scene.view_layer_remove_lightgroup", icon='REMOVE', text="") + sub.separator() + sub.menu("VIEWLAYER_MT_lightgroup_sync", icon='DOWNARROW_HLT', text="") + + + # Get lights from the active view layer + if scene.light_editor_filter: + lights = [obj for obj in context.view_layer.objects + if obj.type == 'LIGHT' and re.search(scene.light_editor_filter.lower(), obj.name.lower(), re.I)] + # if obj.type == 'LIGHT' and fnmatch.fnmatch(obj.name.lower(), scene.light_editor_filter.lower())] + else: + lights = [obj for obj in context.view_layer.objects if obj.type == 'LIGHT'] + if scene.filter_light_types == 'COLLECTION': - all_layer_colls = [] - gather_layer_collections(context.view_layer.layer_collection, all_layer_colls) - relevant_layer_colls = [] - for lc in all_layer_colls: - coll = lc.collection - if coll.name == "Scene Collection": - continue - if any(obj.type == 'LIGHT' for obj in coll.all_objects): - relevant_layer_colls.append(lc) - if not relevant_layer_colls: - box = layout.box() - box.label(text="No Collections Found", icon='ERROR') - else: - for lc in relevant_layer_colls: - coll = lc.collection - coll_name = coll.name - group_key = f"coll_{coll_name}" + # Group by collection logic + groups = {} + for obj in lights: + if obj.users_collection: + group_name = obj.users_collection[0].name + else: + group_name = "No Collection" + groups.setdefault(group_name, []).append(obj) + for group_name, group_objs in groups.items(): + group_key = f"coll_{group_name}" + collapsed = group_collapse_dict.get(group_key, False) + header_box = layout.box() + header_row = header_box.row(align=True) + is_on_1 = group_checkbox_1_state.get(group_key, True) + icon_1 = 'CHECKBOX_HLT' if is_on_1 else 'CHECKBOX_DEHLT' + header_row.active = is_on_1 == True + op_1 = header_row.operator("light_editor.toggle_group_all_off", + text="", + icon=icon_1, + depress=is_on_1) + op_1.group_key = group_key + is_on_2 = group_checkbox_2_state.get(group_key, False) + icon_2 = 'RADIOBUT_ON' if is_on_2 else 'RADIOBUT_OFF' + op_2 = header_row.operator("light_editor.toggle_group_exclusive", + text="", + icon=icon_2, + depress=is_on_2) + op_2.group_key = group_key + op_tri = header_row.operator("light_editor.toggle_group", + text="", + emboss=True, + icon='RIGHTARROW' if collapsed else 'DOWNARROW_HLT') + op_tri.group_key = group_key + header_row.label(text=group_name, icon='OUTLINER_COLLECTION') + + if not collapsed and not scene.filter_light_types == 'GROUP': + for obj in group_objs: + draw_main_row(header_box, obj) + if obj.light_expanded: + extra_box = header_box.box() + # extra_box.label(text="Extra Parameters:") + draw_extra_params(self, extra_box, obj, obj.data) + + elif scene.filter_light_types == 'KIND': + # Group by kind logic + groups = {'POINT': [], 'SPOT': [], 'SUN': [], 'AREA': []} + for obj in lights: + if obj.data.type in groups: + groups[obj.data.type].append(obj) + for kind in ('POINT', 'SPOT', 'SUN', 'AREA'): + if groups[kind]: + group_key = f"kind_{kind}" collapsed = group_collapse_dict.get(group_key, False) header_box = layout.box() header_row = header_box.row(align=True) - is_included = not lc.exclude - icon_1 = 'CHECKBOX_HLT' if is_included else 'CHECKBOX_DEHLT' - header_row.active = True - op_1 = header_row.operator("light_editor.toggle_collection", + is_on_1 = group_checkbox_1_state.get(group_key, True) + icon_1 = 'CHECKBOX_HLT' if is_on_1 else 'CHECKBOX_DEHLT' + header_row.active = is_on_1 == True + op_1 = header_row.operator("light_editor.toggle_group_all_off", text="", icon=icon_1, - depress=is_included) + depress=is_on_1) op_1.group_key = group_key - # Exclusive toggle button: icon is RADIOBUT_ON when active, RECORD_OFF when off. is_on_2 = group_checkbox_2_state.get(group_key, False) - icon_2 = 'RADIOBUT_ON' if is_on_2 else 'RECORD_OFF' - op_excl = header_row.operator("light_editor.toggle_group_exclusive", - text="", - icon=icon_2, - depress=is_on_2) - op_excl.group_key = group_key + icon_2 = 'RADIOBUT_ON' if is_on_2 else 'RADIOBUT_OFF' + op_2 = header_row.operator("light_editor.toggle_group_exclusive", + text="", + icon=icon_2, + depress=is_on_2) + op_2.group_key = group_key op_tri = header_row.operator("light_editor.toggle_group", text="", emboss=True, icon='RIGHTARROW' if collapsed else 'DOWNARROW_HLT') op_tri.group_key = group_key - header_row.label(text=coll_name, icon='OUTLINER_COLLECTION') + header_row.label(text=f"{kind} Lights", icon=f"LIGHT_{kind}") if not collapsed: - lights_in_coll = [o for o in coll.all_objects if o.type == 'LIGHT'] - filter_str = scene.light_editor_filter.strip() - if filter_str: - lights_in_coll = [o for o in lights_in_coll if re.search(filter_str.lower(), o.name.lower())] - for obj in lights_in_coll: + for obj in groups[kind]: draw_main_row(header_box, obj) if obj.light_expanded: extra_box = header_box.box() + # extra_box.label(text="Extra Parameters:") draw_extra_params(self, extra_box, obj, obj.data) - - # KIND mode: group by kind - elif scene.filter_light_types == 'KIND': - if scene.light_editor_filter: - lights = [obj for obj in context.view_layer.objects if obj.type == 'LIGHT' and re.search(scene.light_editor_filter.lower(), obj.name.lower(), re.I)] - else: - lights = [obj for obj in context.view_layer.objects if obj.type == 'LIGHT'] - groups = {'POINT': [], 'SPOT': [], 'SUN': [], 'AREA': []} + + elif scene.filter_light_types == 'GROUP': + groups = {} for obj in lights: - if obj.data.type in groups: - groups[obj.data.type].append(obj) - if any(groups.values()): - for kind in ('POINT', 'SPOT', 'SUN', 'AREA'): - group_objs = groups[kind] - if group_objs: - group_key = f"kind_{kind}" - collapsed = group_collapse_dict.get(group_key, False) - header_box = layout.box() - header_row = header_box.row(align=True) - is_on_1 = group_checkbox_1_state.get(group_key, True) - icon_1 = 'CHECKBOX_HLT' if is_on_1 else 'CHECKBOX_DEHLT' - header_row.active = is_on_1 - op_1 = header_row.operator("light_editor.toggle_kind", - text="", - icon=icon_1, - depress=is_on_1) - op_1.group_key = group_key - # Exclusive toggle button for kind: icon is RADIOBUT_ON when active, RECORD_OFF when off. - is_on_2 = group_checkbox_2_state.get(group_key, False) - icon_2 = 'RADIOBUT_ON' if is_on_2 else 'RECORD_OFF' - op_excl_kind = header_row.operator("light_editor.toggle_group_exclusive", - text="", - icon=icon_2, - depress=is_on_2) - op_excl_kind.group_key = group_key - op_tri = header_row.operator("light_editor.toggle_group", - text="", - emboss=True, - icon='RIGHTARROW' if collapsed else 'DOWNARROW_HLT') - op_tri.group_key = group_key - header_row.label(text=f"{kind} Lights", icon=f"LIGHT_{kind}") - if not collapsed: - for obj in group_objs: - draw_main_row(header_box, obj) - if obj.light_expanded: - extra_box = header_box.box() - draw_extra_params(self, extra_box, obj, obj.data) - else: - box = layout.box() - box.label(text="No Lights Found", icon='ERROR') - - # ALPHABETICAL mode: list all lights sorted by name + # Try the migrated property; fallback to old property if needed. + grp = getattr(obj, "lightgroup", "") + if not grp: + grp = getattr(obj, "lightgroup", "") + if not grp: + grp = "Not Assigned" + groups.setdefault(grp, []).append(obj) + for grp_name, group_objs in groups.items(): + group_key = f"group_{grp_name}" + collapsed = group_collapse_dict.get(group_key, False) + header_box = layout.box() + header_row = header_box.row(align=True) + is_on_1 = group_checkbox_1_state.get(group_key, True) + header_row.active = is_on_1 == True + icon_1 = 'CHECKBOX_HLT' if is_on_1 else 'CHECKBOX_DEHLT' + op_1 = header_row.operator("light_editor.toggle_group_all_off", + text="", + icon=icon_1, + depress=is_on_1) + op_1.group_key = group_key + is_on_2 = group_checkbox_2_state.get(group_key, False) + icon_2 = 'RADIOBUT_ON' if is_on_2 else 'RADIOBUT_OFF' + op_2 = header_row.operator("light_editor.toggle_group_exclusive", + text="", + icon=icon_2, + depress=is_on_2) + op_2.group_key = group_key + op_tri = header_row.operator("light_editor.toggle_group", + text="", + emboss=True, + icon='RIGHTARROW' if collapsed else 'DOWNARROW_HLT') + op_tri.group_key = group_key + header_row.label(text=f"{grp_name}", icon='GROUP') + if not collapsed: + for obj in group_objs: + draw_main_row(header_box, obj) + # We dont show extra light props in this mode + # if obj.light_expanded: + # extra_box = header_box.box() + # # extra_box.label(text="Extra Parameters:") + # draw_extra_params(self, extra_box, obj, obj.data) else: - if scene.light_editor_filter: - lights = [obj for obj in context.view_layer.objects if obj.type == 'LIGHT' and re.search(scene.light_editor_filter.lower(), obj.name.lower(), re.I)] - else: - lights = [obj for obj in context.view_layer.objects if obj.type == 'LIGHT'] - if lights: - box = layout.box() - box.label(text="All Lights (Alphabetical)", icon='LIGHT_DATA') - sorted_lights = sorted(lights, key=lambda o: o.name.lower()) - for obj in sorted_lights: - draw_main_row(box, obj) - if obj.light_expanded: - extra_box = box.box() - draw_extra_params(self, extra_box, obj, obj.data) - else: - box = layout.box() - box.label(text="No Lights Found", icon='ERROR') + # Alphabetical order mode + sorted_lights = sorted(lights, key=lambda o: o.name.lower()) + box = layout.box() + box.label(text="All Lights (Alphabetical)", icon='LIGHT_DATA') + for obj in sorted_lights: + draw_main_row(box, obj) + if obj.light_expanded: # Check if the light is expanded + extra_box = box.box() + # extra_box.label(text="Extra Parameters:") + draw_extra_params(self, extra_box, obj, obj.data) # Draw extra parameters + -# ------------------------------------------------------------------------- -# Handlers -# ------------------------------------------------------------------------- @persistent def LE_check_lights_enabled(dummy): for obj in bpy.data.objects: if obj.type == 'LIGHT': if (obj.hide_viewport and obj.hide_render): - if obj.name in bpy.context.view_layer.objects: - bpy.context.view_layer.objects[obj.name].light_enabled = False + bpy.context.view_layer.objects[obj.name].light_enabled = False else: - if obj.name in bpy.context.view_layer.objects: - bpy.context.view_layer.objects[obj.name].light_enabled = True + bpy.context.view_layer.objects[obj.name].light_enabled = True + @persistent def LE_clear_handler(dummy): context = bpy.context - for obj in bpy.data.objects: - if obj.type == 'LIGHT': - if not (obj.hide_viewport or obj.hide_render): - if obj.name in context.view_layer.objects: + # Migration: for each light, if it has an old "lightgroup" attribute, migrate it. + if bpy and bpy.data: + for obj in bpy.data.objects: + if obj.type == 'LIGHT': + old_group = getattr(obj, "lightgroup", "") + if old_group: + obj.lightgroup = old_group + # obj.custom_lightgroup_menu = obj.lightgroup if obj.lightgroup else "NoGroup" + # if old_group: + # obj.custom_lightgroup = old_group + # obj.custom_lightgroup_menu = obj.custom_lightgroup if obj.custom_lightgroup else "NoGroup" + + # Set enable correct + if (obj.hide_viewport == False and obj.hide_render == False): context.view_layer.objects[obj.name].light_enabled = True - else: - if obj.name in context.view_layer.objects: + else: context.view_layer.objects[obj.name].light_enabled = False + # self.hide_render = not self.light_enabled + -# ------------------------------------------------------------------------- -# Icon Loading -# ------------------------------------------------------------------------- def icon_Load(): + # importing icons import bpy.utils.previews global custom_icons custom_icons = bpy.utils.previews.new() + + # path to the folder where the icon is + # the path is calculated relative to this py file inside the addon folder icons_dir = os.path.join(os.path.dirname(__file__), 'icons') + + # load a preview thumbnail of a file and store in the previews collection custom_icons.load("SELECT_TRUE", os.path.join(icons_dir, "select_true.png"), 'IMAGE') custom_icons.load("SELECT_FALSE", os.path.join(icons_dir, "select_false.png"), 'IMAGE') +# global variable to store icons in custom_icons = None -# ------------------------------------------------------------------------- -# Registration -# ------------------------------------------------------------------------- +# Register the new operator and property classes = ( LIGHT_OT_ToggleGroup, - LIGHT_OT_ToggleCollection, - LIGHT_OT_ToggleKind, + LIGHT_OT_ToggleGroupAllOff, LIGHT_OT_ToggleGroupExclusive, + # LIGHT_OT_CreateNewLightgroup, + # LIGHT_OT_DeleteLightGroup, LIGHT_OT_ClearFilter, LIGHT_OT_SelectLight, LIGHT_PT_editor, @@ -675,47 +940,90 @@ def icon_Load(): def register(): icon_Load() - print("Light Editor add-on registered successfully.") + # Register scene properties bpy.types.Scene.current_active_light = bpy.props.PointerProperty(type=bpy.types.Object) bpy.types.Scene.current_exclusive_group = bpy.props.StringProperty() bpy.types.Scene.selected_render_layer = EnumProperty( name="Render Layer", description="Select the render layer", - items=get_render_layer_items, + items=get_render_layer_items, # Your function that lists view layers. update=update_render_layer ) + + # Register other classes and properties for cls in classes: bpy.utils.register_class(cls) + + # Register other properties (as shown in your original code) bpy.types.Scene.light_editor_filter = StringProperty( name="Filter", default="", - description="Filter lights by name (regex allowed)" + description="Filter lights by name (wildcards allowed)" ) bpy.types.Scene.light_editor_kind_alpha = BoolProperty( name="By Kind", description="Group lights by kind", - default=False, + default=False, # Ensure this is False by default update=update_group_by_kind ) bpy.types.Scene.light_editor_group_by_collection = BoolProperty( name="By Collections", description="Group lights by collection", - default=False, + default=False, # Ensure this is False by default update=update_group_by_collection ) + bpy.types.Scene.light_editor_light_group = BoolProperty( + name="By Light Groups", + description="Group lights by their custom light group assignment", + default=False, # Ensure this is False by default + update=update_light_group + ) + # bpy.types.Scene.create_new_lightgroup = StringProperty( + # name="", + # default="", + # description="Enter a new light group name" + # ) + bpy.types.Scene.custom_lightgroups_list = StringProperty( + name="Custom Lightgroups", + default="", + description="Comma separated list of custom light groups created without an active light" + ) + + # bpy.types.Scene.light_editor_delete_group = EnumProperty( + # name="Delete Light Group", + # description="Select a light group to delete", + # items=get_light_group_items # Pass the function directly (no parentheses) + # ) + bpy.types.Scene.filter_light_types = EnumProperty( name="Type", description="Filter light by type", - items=(('NO_FILTER', 'All', 'Show All (Alphabetical)', 'NONE', 0), - ('KIND', 'Kind', 'Filter lights by Kind', 'LIGHT_DATA', 1), - ('COLLECTION', 'Collection', 'Filter lights by Collections', 'OUTLINER_COLLECTION', 2)), - default='NO_FILTER' + items=(('NO_FILTER', 'All', 'Show All no filter (Alphabetical)', 'NONE', 0), ('KIND', 'Kind', 'FIlter lights by Kind', 'LIGHT_DATA', 1),('COLLECTION', 'Collection', 'FIlter lights by Collections', 'OUTLINER_COLLECTION', 2),('GROUP', 'Light Group', 'Filter lights by Light Groups', 'GROUP', 3)), + update=update_light_types ) + + # Register Light properties bpy.types.Light.soft_falloff = BoolProperty(default=False) bpy.types.Light.max_bounce = IntProperty(default=0, min=0, max=10) bpy.types.Light.multiple_instance = BoolProperty(default=False) bpy.types.Light.shadow_caustic = BoolProperty(default=False) bpy.types.Light.spread = FloatProperty(default=0.0, min=0.0, max=1.0) + + + # Register custom properties on Object using unique names + # bpy.types.Object.custom_lightgroup = StringProperty( + # name="Light Group", + # description="The custom lightgroup attribute for this object", + # default="" + # ) + + # bpy.types.Object.custom_lightgroup_menu = EnumProperty( + # name="Light Group", + # description="Select the custom light group for this light", + # items=get_light_group_items, # Pass the function directly (no parentheses) + # update=update_lightgroup_menu + # ) + bpy.types.Object.light_enabled = BoolProperty( name="Enabled", default=True, @@ -728,53 +1036,52 @@ def register(): ) bpy.types.Object.light_expanded = BoolProperty( name="Expanded", - default=False + default=False # Ensure this is False by default ) + + #add handler post load > see @persistent bpy.app.handlers.load_post.append(LE_clear_handler) + # bpy.app.handlers.depsgraph_update_post.append(LE_check_lights_enabled) bpy.app.handlers.load_post.append(LE_check_lights_enabled) + + + def unregister(): global custom_icons - if custom_icons: - bpy.utils.previews.remove(custom_icons) - custom_icons = None - if LE_clear_handler in bpy.app.handlers.load_post: - bpy.app.handlers.load_post.remove(LE_clear_handler) - if LE_check_lights_enabled in bpy.app.handlers.load_post: - bpy.app.handlers.load_post.remove(LE_check_lights_enabled) - if hasattr(bpy.types.Scene, 'current_active_light'): - del bpy.types.Scene.current_active_light - if hasattr(bpy.types.Scene, 'current_exclusive_group'): - del bpy.types.Scene.current_exclusive_group - if hasattr(bpy.types.Scene, 'selected_render_layer'): - del bpy.types.Scene.selected_render_layer - if hasattr(bpy.types.Scene, 'light_editor_filter'): - del bpy.types.Scene.light_editor_filter - if hasattr(bpy.types.Scene, 'light_editor_kind_alpha'): - del bpy.types.Scene.light_editor_kind_alpha - if hasattr(bpy.types.Scene, 'light_editor_group_by_collection'): - del bpy.types.Scene.light_editor_group_by_collection - if hasattr(bpy.types.Scene, 'filter_light_types'): - del bpy.types.Scene.filter_light_types - if hasattr(bpy.types.Light, 'soft_falloff'): - del bpy.types.Light.soft_falloff - if hasattr(bpy.types.Light, 'max_bounce'): - del bpy.types.Light.max_bounce - if hasattr(bpy.types.Light, 'multiple_instance'): - del bpy.types.Light.multiple_instance - if hasattr(bpy.types.Light, 'shadow_caustic'): - del bpy.types.Light.shadow_caustic - if hasattr(bpy.types.Light, 'spread'): - del bpy.types.Light.spread - if hasattr(bpy.types.Object, 'light_enabled'): - del bpy.types.Object.light_enabled - if hasattr(bpy.types.Object, 'light_turn_off_others'): - del bpy.types.Object.light_turn_off_others - if hasattr(bpy.types.Object, 'light_expanded'): - del bpy.types.Object.light_expanded + bpy.utils.previews.remove(custom_icons) + + #removove handler post load > see @persistent + bpy.app.handlers.load_post.remove(LE_clear_handler) + bpy.app.handlers.load_post.remove(LE_check_lights_enabled) + + # Unregister scene properties + del bpy.types.Scene.current_active_light + del bpy.types.Scene.current_exclusive_group + del bpy.types.Scene.selected_render_layer + + # Unregister other classes and properties for cls in reversed(classes): - if hasattr(bpy.types, cls.__name__): - bpy.utils.unregister_class(cls) + bpy.utils.unregister_class(cls) + + # Unregister other properties (as shown in your original code) + del bpy.types.Scene.light_editor_filter + del bpy.types.Scene.light_editor_kind_alpha + del bpy.types.Scene.light_editor_group_by_collection + del bpy.types.Scene.light_editor_light_group + # del bpy.types.Scene.create_new_lightgroup + # del bpy.types.Scene.custom_lightgroups_list + del bpy.types.Scene.light_editor_delete_group + del bpy.types.Light.soft_falloff + del bpy.types.Light.max_bounce + del bpy.types.Light.multiple_instance + del bpy.types.Light.shadow_caustic + del bpy.types.Light.spread + # del bpy.types.Object.custom_lightgroup + # del bpy.types.Object.custom_lightgroup_menu + del bpy.types.Object.light_enabled + del bpy.types.Object.light_turn_off_others + del bpy.types.Object.light_expanded if __name__ == "__main__": register() From 3a787b438a0973356e84039e32c3a0d7810033e7 Mon Sep 17 00:00:00 2001 From: schroef Date: Wed, 19 Feb 2025 10:26:33 -0400 Subject: [PATCH 4/6] Revert "update from stream" This reverts commit 3447a29481dea156bf9b865bced8882c78bb451a. --- LightEditor.py | 1019 +++++++++++++++++------------------------------- 1 file changed, 356 insertions(+), 663 deletions(-) diff --git a/LightEditor.py b/LightEditor.py index c2a99c8..dfe09f4 100644 --- a/LightEditor.py +++ b/LightEditor.py @@ -15,93 +15,54 @@ current_active_light = None current_exclusive_group = None -# Dictionaries to store states -group_checkbox_1_state = {} # First button on/off per group_key (ON by default) +# Global dictionaries to track states +group_checkbox_1_state = {} # Used for first button (toggle group on/off) group_lights_original_state = {} # Stores original light states for each group -group_checkbox_2_state = {} # Second button on/off per group_key (OFF by default) -other_groups_original_state = {} # Stores other groups' states group_collapse_dict = {} # Whether each group is collapsed +collections_with_lights = {} # Tracks collections that contain lights -# ------------------------------------------------------------------------- -# Add a new operator for deleting a light group -# ------------------------------------------------------------------------- -class LIGHT_OT_DeleteLightGroup(bpy.types.Operator): - """Delete the selected light group.""" - bl_idname = "light_editor.delete_light_group" - bl_label = "Delete Light Group" - - def execute(self, context): - scene = context.scene - selected_group = scene.light_editor_delete_group - - if selected_group == "NoGroup": - self.report({'WARNING'}, "No group selected.") - return {'CANCELLED'} - - # Remove the group from all lights (use context.view_layer.objects) - for obj in context.view_layer.objects: - if obj.type == 'LIGHT' and getattr(obj, "custom_lightgroup", "") == selected_group: - obj.custom_lightgroup = "" - obj.custom_lightgroup_menu = "NoGroup" - - # Remove the group from the scene's custom_lightgroups_list - if hasattr(scene, "custom_lightgroups_list") and scene.custom_lightgroups_list: - groups = [g.strip() for g in scene.custom_lightgroups_list.split(",") if g.strip()] - if selected_group in groups: - groups.remove(selected_group) - scene.custom_lightgroups_list = ", ".join(groups) - - self.report({'INFO'}, f"Deleted light group '{selected_group}'") - return {'FINISHED'} +# New globals for the exclusive (second) button +group_checkbox_2_state = {} # Exclusive button state per group (False by default) +other_groups_original_state = {} # Stores the other groups' light states # ------------------------------------------------------------------------- -# 1) Functions for custom light group handling +# Render layer functions # ------------------------------------------------------------------------- -def get_light_groups(context): - """ - Loop through all objects, find lights, and collect them by their custom light group. - If a light already has an old "lightgroup" value, it is used. - Also include any groups stored in the scene property. - Returns a dictionary mapping group names to lists of light object names. - """ - groups = {} - for obj in context.view_layer.objects: - if obj.type == 'LIGHT': - # Try the new property; if empty, fallback to old property. - group_val = getattr(obj, "lightgroup", "") - if not group_val: - group_val = getattr(obj, "lightgroup", "") - if group_val: - groups.setdefault(group_val, []).append(obj.name) - # Get any groups created via the operator (even if no light has that assignment) - scene = context.scene - if hasattr(scene, "custom_lightgroups_list") and scene.custom_lightgroups_list: - extra_groups = [g.strip() for g in scene.custom_lightgroups_list.split(",") if g.strip()] - for g in extra_groups: - if g not in groups: - groups[g] = [] # no lights assigned yet - return groups - -def get_light_group_items(self, context): - """Return a list of custom light group items for the EnumProperty.""" - items = [("NoGroup", "No Group", "")] - groups = get_light_groups(context) # Pass context to get_light_groups - for group_name in groups.keys(): - if group_name != "NoGroup": - items.append((group_name, group_name, "")) - return items - -def update_lightgroup_menu(self, context): - """When the menu selection changes, copy the value into our custom_lightgroup property.""" - self.custom_lightgroup = self.custom_lightgroup_menu +def update_render_layer(self, context): + selected = self.selected_render_layer + for vl in context.scene.view_layers: + if vl.name == selected: + context.window.view_layer = vl + break -def view_layer_items(self, context): - """Return a list of view layer items for the EnumProperty.""" +def get_render_layer_items(self, context): items = [] for view_layer in context.scene.view_layers: items.append((view_layer.name, view_layer.name, "")) return items +# ------------------------------------------------------------------------- +# Collection functions +# ------------------------------------------------------------------------- +def gather_layer_collections(parent_lc, result): + """Recursively gather all LayerCollections in the parent_lc hierarchy.""" + result.append(parent_lc) + for child in parent_lc.children: + gather_layer_collections(child, result) + +def get_layer_collection_by_name(layer_collection, coll_name): + """Recursively search for a LayerCollection with the given name.""" + if layer_collection.collection.name == coll_name: + return layer_collection + for child in layer_collection.children: + found = get_layer_collection_by_name(child, coll_name) + if found: + return found + return None + +# ------------------------------------------------------------------------- +# Light update functions +# ------------------------------------------------------------------------- def update_light_enabled(self, context): self.hide_viewport = not self.light_enabled self.hide_render = not self.light_enabled @@ -112,7 +73,6 @@ def update_light_turn_off_others(self, context): if scene.current_active_light and scene.current_active_light != self: scene.current_active_light.light_turn_off_others = False scene.current_active_light = self - # Use objects from the active view layer for obj in context.view_layer.objects: if obj.type == 'LIGHT' and obj.name != self.name: if 'prev_light_enabled' not in obj: @@ -127,35 +87,35 @@ def update_light_turn_off_others(self, context): obj.light_enabled = obj['prev_light_enabled'] del obj['prev_light_enabled'] -# --- MUTUALLY EXCLUSIVE GROUPING TOGGLES --- +def get_all_collections(obj): + def _get_collections_recursive(collection, path=None): + if path is None: + path = [] + path.append(collection.name) + yield path[:] + for child in collection.children: + yield from _get_collections_recursive(child, path) + path.pop() + all_collections = set() + for collection in obj.users_collection: + for path in _get_collections_recursive(collection): + all_collections.add(" > ".join(path)) + return sorted(all_collections) + +# ------------------------------------------------------------------------- +# Mutually exclusive grouping toggles +# ------------------------------------------------------------------------- def update_group_by_kind(self, context): if self.light_editor_kind_alpha: self.light_editor_group_by_collection = False - self.light_editor_light_group = False def update_group_by_collection(self, context): if self.light_editor_group_by_collection: self.light_editor_kind_alpha = False - self.light_editor_light_group = False - -def update_light_group(self, context): - if self.light_editor_light_group: - self.light_editor_kind_alpha = False - self.light_editor_group_by_collection = False - - # Collapse all expanded lights when switching to "By Light Groups" mode - for obj in context.view_layer.objects: - if obj.type == 'LIGHT': - obj.light_expanded = False -def update_light_types(self, context): - if self == 'group': - # Collapse all expanded lights when switching to "By Light Groups" mode - for obj in context.view_layer.objects: - if obj.type == 'LIGHT': - obj.light_expanded = False - -# Used for MacOS +# ------------------------------------------------------------------------- +# Device functions (for cycles) +# ------------------------------------------------------------------------- def get_device_type(context): return context.preferences.addons['cycles'].preferences.compute_device_type @@ -164,11 +124,9 @@ def backend_has_active_gpu(context): def use_metal(context): cscene = context.scene.cycles - return (get_device_type(context) == 'METAL' and cscene.device == 'GPU' and backend_has_active_gpu(context)) def use_mnee(context): - # The MNEE kernel doesn't compile on macOS < 13. if use_metal(context): import platform version, _, _ = platform.mac_ver() @@ -178,21 +136,15 @@ def use_mnee(context): return True # ------------------------------------------------------------------------- -# 2) Extra parameters drawing function (unchanged) +# Extra parameters drawing function (unchanged) # ------------------------------------------------------------------------- def draw_extra_params(self, box, obj, light): - # Draw the default light properties UI in the provided box if light and isinstance(light, bpy.types.Light): - # Copy the layout drawing logic from DATA_PT_light layout = box - - # Display the 4 light type buttons in a single row row = layout.row() row.prop(light, "type", expand=True) - col = layout.column() col.separator() - if bpy.context.engine == 'CYCLES': clamp = light.cycles if light.type in {'POINT', 'SPOT'}: @@ -203,29 +155,23 @@ def draw_extra_params(self, box, obj, light): elif light.type == 'AREA': col.prop(light, "shape", text="Shape") sub = col.column(align=True) - if light.shape in {'SQUARE', 'DISK'}: sub.prop(light, "size") elif light.shape in {'RECTANGLE', 'ELLIPSE'}: sub.prop(light, "size", text="Size X") sub.prop(light, "size_y", text="Y") - if not (light.type == 'AREA' and clamp.is_portal): col.separator() sub = col.column() sub.prop(clamp, "max_bounces") - sub = col.column(align=True) sub.active = not (light.type == 'AREA' and clamp.is_portal) - sub.prop(light, "use_shadow", text="Cast Shadow") sub.prop(clamp, "use_multiple_importance_sampling", text="Multiple Importance") if use_mnee(bpy.context): sub.prop(clamp, "is_caustics_light", text="Shadow Caustics") - if light.type == 'AREA': col.prop(clamp, "is_portal", text="Portal") - if light.type == 'SPOT': col.separator() row = col.row(align=True) @@ -234,15 +180,12 @@ def draw_extra_params(self, box, obj, light): col.prop(light, "spot_size", text="Spot Size") col.prop(light, "spot_blend", text="Blend", slider=True) col.prop(light, "show_cone") - elif light.type == 'AREA': col.separator() row = col.row(align=True) row.alignment = 'CENTER' row.label(text="Beam Shape") col.prop(light, "spread", text="Spread") - - # EEVEE if ((bpy.context.engine == 'BLENDER_EEVEE') or (bpy.context.engine == 'BLENDER_EEVEE_NEXT')): col.separator() if light.type in {'POINT', 'SPOT'}: @@ -252,15 +195,12 @@ def draw_extra_params(self, box, obj, light): col.prop(light, "angle") elif light.type == 'AREA': col.prop(light, "shape") - sub = col.column(align=True) - if light.shape in {'SQUARE', 'DISK'}: sub.prop(light, "size") elif light.shape in {'RECTANGLE', 'ELLIPSE'}: sub.prop(light, "size", text="Size X") sub.prop(light, "size_y", text="Y") - if bpy.context.engine == 'BLENDER_EEVEE_NEXT': col.separator() col.prop(light, "use_shadow", text="Cast Shadow") @@ -268,8 +208,7 @@ def draw_extra_params(self, box, obj, light): col.prop(light, "shadow_jitter_overblur", text="Overblur") col.prop(light, "shadow_filter_radius", text="Radius") col.prop(light, "shadow_maximum_resolution", text="Resolution Limit") - - if (light and light.type == 'SPOT'): + if light and light.type == 'SPOT': col.separator() row = col.row(align=True) row.alignment = 'CENTER' @@ -277,80 +216,25 @@ def draw_extra_params(self, box, obj, light): col.prop(light, "spot_size", text="Size") col.prop(light, "spot_blend", text="Blend", slider=True) col.prop(light, "show_cone") - col.separator() col.prop(light, "diffuse_factor", text="Diffuse") col.prop(light, "specular_factor", text="Specular") col.prop(light, "volume_factor", text="Volume", text_ctxt=i18n_contexts.id_id) - - if (light and light.type != 'SUN'): + if light.type != 'SUN': col.separator() sub = col.column() sub.prop(light, "use_custom_distance", text="Custom Distance") sub.active = light.use_custom_distance sub.prop(light, "cutoff_distance", text="Distance") -# ------------------------------------------------------------------------- -# Render layer menu -# ------------------------------------------------------------------------- - -def get_render_layer_items(self, context): - """Return a list of render layer items for the EnumProperty.""" - items = [] - for view_layer in context.scene.view_layers: - items.append((view_layer.name, view_layer.name, "")) - return items - -def update_render_layer(self, context): - selected = self.selected_render_layer - # Iterate over the scene’s view layers: - for vl in context.scene.view_layers: - if vl.name == selected: - context.window.view_layer = vl - break - - -# ------------------------------------------------------------------------- -# 3) Operator: Add Lightgroup -# This operator now works even if no light is selected. -# When executed it assigns the new group name to the active light (if one exists) -# or adds the group name to the scene's custom_lightgroups_list. -# ------------------------------------------------------------------------- -class LIGHT_OT_CreateNewLightgroup(bpy.types.Operator): - """Add a new lightgroup and assign it (if a light is selected) or store it for later use.""" - bl_idname = "light_editor.create_new_lightgroup" - bl_label = "Add Lightgroup" - - def execute(self, context): - scene = context.scene - new_group = scene.create_new_lightgroup.strip() - if not new_group: - self.report({'WARNING'}, "No group name entered.") - return {'CANCELLED'} - active_obj = context.active_object - if active_obj and active_obj.type == 'LIGHT': - active_obj.custom_lightgroup = new_group - active_obj.custom_lightgroup_menu = new_group - self.report({'INFO'}, f"Assigned group '{new_group}' to {active_obj.name}") - else: - # No active light: add the group name to the scene list (if not already present) - current_list = scene.custom_lightgroups_list - groups = [g.strip() for g in current_list.split(",") if g.strip()] if current_list else [] - if new_group not in groups: - groups.append(new_group) - scene.custom_lightgroups_list = ", ".join(groups) - self.report({'INFO'}, f"Created light group '{new_group}'") - scene.create_new_lightgroup = "" - return {'FINISHED'} # ------------------------------------------------------------------------- -# 4) Operators for group toggling (object access updated) +# Operators # ------------------------------------------------------------------------- class LIGHT_OT_ToggleGroup(bpy.types.Operator): """Collapse or expand a group header.""" bl_idname = "light_editor.toggle_group" bl_label = "Toggle Group" - group_key: StringProperty() - + group_key: bpy.props.StringProperty() def execute(self, context): current = group_collapse_dict.get(self.group_key, False) group_collapse_dict[self.group_key] = not current @@ -359,12 +243,42 @@ def execute(self, context): area.tag_redraw() return {'FINISHED'} -class LIGHT_OT_ToggleGroupAllOff(bpy.types.Operator): - """Toggle all lights in the group off or restore them.""" - bl_idname = "light_editor.toggle_group_all_off" - bl_label = "Toggle Group All Off" - group_key: StringProperty() +class LIGHT_OT_ToggleCollection(bpy.types.Operator): + """Toggle the exclusion state of a collection (and its sub-collections).""" + bl_idname = "light_editor.toggle_collection" + bl_label = "Toggle Collection Exclusion" + group_key: bpy.props.StringProperty() + def execute(self, context): + global collections_with_lights + if self.group_key.startswith("coll_"): + coll_name = self.group_key[5:] + collection = bpy.data.collections.get(coll_name) + if not collection: + return {'CANCELLED'} + def toggle_exclusion_recursive(layer_coll, exclude): + layer_coll.exclude = exclude + for child in layer_coll.children: + toggle_exclusion_recursive(child, exclude) + layer_collection = get_layer_collection_by_name(context.view_layer.layer_collection, coll_name) + if layer_collection: + current_excluded = layer_collection.exclude + toggle_exclusion_recursive(layer_collection, not current_excluded) + def has_lights(coll): + return any(obj.type == 'LIGHT' for obj in coll.all_objects) + if has_lights(collection): + collections_with_lights[coll_name] = True + else: + collections_with_lights.pop(coll_name, None) + for area in context.screen.areas: + if area.type == 'VIEW_3D': + area.tag_redraw() + return {'FINISHED'} +class LIGHT_OT_ToggleKind(bpy.types.Operator): + """Toggle all lights of a specific kind.""" + bl_idname = "light_editor.toggle_kind" + bl_label = "Toggle Kind Visibility" + group_key: bpy.props.StringProperty() def execute(self, context): global group_checkbox_1_state, group_lights_original_state is_on = group_checkbox_1_state.get(self.group_key, True) @@ -372,227 +286,161 @@ def execute(self, context): if is_on: original_states = {} for obj in group_objs: - original_states[obj.name] = obj.light_enabled + if obj.type == 'LIGHT': + original_states[obj.name] = obj.light_enabled + obj.light_enabled = False group_lights_original_state[self.group_key] = original_states - for obj in group_objs: - obj.light_enabled = False - group_checkbox_1_state[self.group_key] = False else: original_states = group_lights_original_state.get(self.group_key, {}) for obj in group_objs: - obj.light_enabled = original_states.get(obj.name, True) + if obj.type == 'LIGHT': + obj.light_enabled = original_states.get(obj.name, True) if self.group_key in group_lights_original_state: del group_lights_original_state[self.group_key] - group_checkbox_1_state[self.group_key] = True + group_checkbox_1_state[self.group_key] = not is_on for area in context.screen.areas: if area.type == 'VIEW_3D': area.tag_redraw() return {'FINISHED'} - def _get_group_objects(self, context, group_key): - scene = context.scene - filter_pattern = scene.light_editor_filter.lower() + filter_pattern = context.scene.light_editor_filter.lower() + all_lights = [obj for obj in context.view_layer.objects if obj.type == 'LIGHT'] if filter_pattern: - # all_lights = [obj for obj in context.view_layer.objects - # if obj.type == 'LIGHT' and fnmatch.fnmatch(obj.name.lower(), filter_pattern)] - - all_lights = [obj for obj in context.view_layer.objects - if obj.type == 'LIGHT' and re.search(filter_pattern, obj.name, re.I)] - - # for fn in sorted(files): - # if not fn.lower().startswith("._"): - # if fn.lower().endswith(".hdr") or fn.lower().endswith(".exr") or fn.lower().endswith(".jpg") or fn.lower().endswith(".png"): - # if not scn.easyhdr_filter or re.search(scn.easyhdr_filter, fn, re.I): - # hdris.append(os.path.join(root, fn).replace(dir, '')) - # no_match = False - else: - all_lights = [obj for obj in context.view_layer.objects if obj.type == 'LIGHT'] - if scene.filter_light_types == 'COLLECTION' and group_key.startswith("coll_"): - coll_name = group_key[5:] - return [obj for obj in all_lights - if (obj.users_collection and obj.users_collection[0].name == coll_name) - or (not obj.users_collection and coll_name == "No Collection")] - if scene.filter_light_types == 'KIND' and group_key.startswith("kind_"): - kind = group_key[5:] - return [obj for obj in all_lights if obj.data.type == kind] - if scene.filter_light_types == 'GROUP' and group_key.startswith("group_"): - group_name = group_key[6:] - return [obj for obj in all_lights if getattr(obj, "custom_lightgroup", "") == group_name] - return [] + all_lights = [obj for obj in all_lights if re.search(filter_pattern, obj.name, re.I)] + kind = group_key[5:] + return [obj for obj in all_lights if obj.data.type == kind] class LIGHT_OT_ToggleGroupExclusive(bpy.types.Operator): """ Toggle exclusive mode for a group. - When turned on, lights in other groups (including "Not Assigned") are turned off. - When turned off, lights in other groups are restored to their previous states. + When turned on, lights in all other groups are turned off. + When turned off, the previous states of the other groups are restored. """ bl_idname = "light_editor.toggle_group_exclusive" bl_label = "Toggle Group Exclusive" - group_key: StringProperty() - + group_key: bpy.props.StringProperty() def execute(self, context): global current_exclusive_group, group_checkbox_2_state, other_groups_original_state - - # Check if the current group is already in exclusive mode is_on = group_checkbox_2_state.get(self.group_key, False) - if not is_on: - # If another group is already in exclusive mode, turn it off first if current_exclusive_group and current_exclusive_group != self.group_key: + saved = other_groups_original_state.get(current_exclusive_group, {}) + for gk, states in saved.items(): + objs = self._get_group_objects(context, gk) + for obj in objs: + obj.light_enabled = states.get(obj.name, True) group_checkbox_2_state[current_exclusive_group] = False - saved_dict = other_groups_original_state.get(current_exclusive_group, {}) - for gk, light_dict in saved_dict.items(): - grp_objs = self._get_group_objects(context, gk) - for obj in grp_objs: - obj.light_enabled = light_dict.get(obj.name, True) if current_exclusive_group in other_groups_original_state: del other_groups_original_state[current_exclusive_group] - - # Turn off all other groups (including "Not Assigned") and store their current states others = self._get_all_other_groups(context, self.group_key) - saved_dict = {} + saved = {} for gk in others: - grp_objs = self._get_group_objects(context, gk) - saved_dict[gk] = {obj.name: obj.light_enabled for obj in grp_objs} - for obj in grp_objs: + objs = self._get_group_objects(context, gk) + saved[gk] = {obj.name: obj.light_enabled for obj in objs} + for obj in objs: obj.light_enabled = False - - # Store the saved states and mark the current group as exclusive - other_groups_original_state[self.group_key] = saved_dict + other_groups_original_state[self.group_key] = saved group_checkbox_2_state[self.group_key] = True current_exclusive_group = self.group_key - else: - # Restore the states of all other groups (including "Not Assigned") - saved_dict = other_groups_original_state.get(self.group_key, {}) - for gk, light_dict in saved_dict.items(): - grp_objs = self._get_group_objects(context, gk) - for obj in grp_objs: - obj.light_enabled = light_dict.get(obj.name, True) - - # Clear the saved states and mark the current group as non-exclusive + saved = other_groups_original_state.get(self.group_key, {}) + for gk, states in saved.items(): + objs = self._get_group_objects(context, gk) + for obj in objs: + obj.light_enabled = states.get(obj.name, True) if self.group_key in other_groups_original_state: del other_groups_original_state[self.group_key] group_checkbox_2_state[self.group_key] = False current_exclusive_group = None - - # Redraw the UI to reflect the changes for area in context.screen.areas: if area.type == 'VIEW_3D': area.tag_redraw() - return {'FINISHED'} - def _get_all_other_groups(self, context, current_key): - """Get all group keys except the current one, including "Not Assigned".""" all_groups = self._get_all_group_keys(context) return [k for k in all_groups if k != current_key] - def _get_all_group_keys(self, context): - """Get all group keys, including "Not Assigned".""" scene = context.scene - filter_pattern = scene.light_editor_filter.lower() - if filter_pattern: - lights = [obj for obj in context.view_layer.objects - if obj.type == 'LIGHT' and re.search(filter_pattern, obj.name, re.I)] - else: - lights = [obj for obj in context.view_layer.objects if obj.type == 'LIGHT'] - - group_keys = set() - if scene.filter_light_types == 'COLLECTION': - for obj in lights: - if obj.users_collection: - coll_name = obj.users_collection[0].name - else: - coll_name = "No Collection" - group_keys.add(f"coll_{coll_name}") - elif scene.filter_light_types == 'KIND': - for obj in lights: - if obj.data.type in {'POINT', 'SPOT', 'SUN', 'AREA'}: - group_keys.add(f"kind_{obj.data.type}") - elif scene.filter_light_types == 'GROUP': - for obj in lights: - group_name = getattr(obj, "lightgroup", "") - if not group_name: - group_name = "Not Assigned" # Explicitly handle "Not Assigned" - group_keys.add(f"group_{group_name}") - return list(group_keys) - + filter_type = scene.filter_light_types + keys = set() + if filter_type == 'COLLECTION': + for obj in context.view_layer.objects: + if obj.type == 'LIGHT': + if obj.users_collection: + keys.add("coll_" + obj.users_collection[0].name) + else: + keys.add("coll_No Collection") + elif filter_type == 'KIND': + for obj in context.view_layer.objects: + if obj.type == 'LIGHT': + keys.add("kind_" + obj.data.type) + elif filter_type == 'GROUP': + for obj in context.view_layer.objects: + if obj.type == 'LIGHT': + grp = getattr(obj, "lightgroup", "") + if not grp: + grp = "Not Assigned" + keys.add("group_" + grp) + return list(keys) def _get_group_objects(self, context, group_key): - """Get all objects in the specified group, including "Not Assigned".""" scene = context.scene filter_pattern = scene.light_editor_filter.lower() + all_lights = [obj for obj in context.view_layer.objects if obj.type == 'LIGHT'] if filter_pattern: - all_lights = [obj for obj in context.view_layer.objects - if obj.type == 'LIGHT' and re.search(filter_pattern, obj.name, re.I)] - else: - all_lights = [obj for obj in context.view_layer.objects if obj.type == 'LIGHT'] - + all_lights = [obj for obj in all_lights if re.search(filter_pattern, obj.name, re.I)] if scene.filter_light_types == 'COLLECTION' and group_key.startswith("coll_"): coll_name = group_key[5:] - return [obj for obj in all_lights - if (obj.users_collection and obj.users_collection[0].name == coll_name) - or (not obj.users_collection and coll_name == "No Collection")] - if scene.filter_light_types == 'KIND' and group_key.startswith("kind_"): + return [obj for obj in all_lights if (obj.users_collection and obj.users_collection[0].name == coll_name) or (not obj.users_collection and coll_name == "No Collection")] + elif scene.filter_light_types == 'KIND' and group_key.startswith("kind_"): kind = group_key[5:] return [obj for obj in all_lights if obj.data.type == kind] - if scene.filter_light_types == 'GROUP' and group_key.startswith("group_"): - group_name = group_key[6:] - if group_name == "Not Assigned": + elif scene.filter_light_types == 'GROUP' and group_key.startswith("group_"): + grp_name = group_key[6:] + if grp_name == "Not Assigned": return [obj for obj in all_lights if not getattr(obj, "lightgroup", "")] else: - return [obj for obj in all_lights if getattr(obj, "lightgroup", "") == group_name] + return [obj for obj in all_lights if getattr(obj, "lightgroup", "") == grp_name] return [] class LIGHT_OT_ClearFilter(bpy.types.Operator): """Clear Filter Types""" bl_idname = "le.clear_light_filter" bl_label = "Clear Filter" - @classmethod - def poll(cls, context): - return context.scene.light_editor_filter - + def description(cls, context, properties): + scene = context.scene + if scene.filter_light_types == 'COLLECTION': + return "Turn ON/OFF Collection" + elif scene.filter_light_types == 'KIND': + return "Turn ON/OFF All Lights of This Kind" + return "Toggle all lights in the group off or restore them" def execute(self, context): - if context.scene.light_editor_filter: - context.scene.light_editor_filter = "" + context.scene.light_editor_filter = "" return {'FINISHED'} - class LIGHT_OT_SelectLight(bpy.types.Operator): """Selects light from UI and deselects everything else""" bl_idname = "le.select_light" bl_label = "Select Light" - - name : StringProperty() # Name of the light to select - + name : StringProperty() def execute(self, context): - # Get the view layer objects vob = context.view_layer.objects - - # Check if the light is already selected if self.name in vob: light = vob[self.name] - if light.select_get(): # If the light is already selected - # Deselect everything + if light.select_get(): bpy.ops.object.select_all(action='DESELECT') self.report({'INFO'}, f"Deselected all objects") else: - # Deselect all objects first bpy.ops.object.select_all(action='DESELECT') - # Select the specified light light.select_set(True) - vob.active = light # Set the light as the active object + vob.active = light self.report({'INFO'}, f"Selected light: {self.name}") else: self.report({'ERROR'}, f"Light '{self.name}' not found") - return {'FINISHED'} - - # ------------------------------------------------------------------------- -# 4) The main row UI: use custom_lightgroup property when in Light Group mode +# Main row UI drawing function # ------------------------------------------------------------------------- def draw_main_row(box, obj): light = obj.data @@ -600,68 +448,32 @@ def draw_main_row(box, obj): scene = bpy.context.scene row = box.row(align=True) controls_row = row.row(align=True) - - # The enabled and "turn off others" toggles remain: controls_row.prop(obj, "light_enabled", text="", icon="OUTLINER_OB_LIGHT" if obj.light_enabled else "LIGHT_DATA") - controls_row.active = obj.light_enabled == True + controls_row.active = (obj.light_enabled == True) controls_row.prop(obj, "light_turn_off_others", text="", icon="RADIOBUT_ON" if obj.light_turn_off_others else "RADIOBUT_OFF") - if not scene.filter_light_types == 'GROUP': selected_true = custom_icons["SELECT_TRUE"].icon_id selected_false = custom_icons["SELECT_FALSE"].icon_id - controls_row.operator("le.select_light", text="", - icon_value=selected_true if obj.select_get() == True else selected_false).name = obj.name - - # -- Only show the triangle if NOT in Light Group mode -- + controls_row.operator("le.select_light", text="", + icon_value=selected_true if obj.select_get() else selected_false).name = obj.name if not scene.filter_light_types == 'GROUP': controls_row.prop(obj, "light_expanded", text="", emboss=True, icon='DOWNARROW_HLT' if obj.light_expanded else 'RIGHTARROW') - - # Apply 50-50 split only in Light Group mode - if scene.filter_light_types == 'GROUP': - split = row.split(factor=0.5) - col_name = split.column(align=True) - col_name.prop(obj, "name", text="") # Default width in Light Group mode - - col_group = split.column(align=True) - view_layer = context.view_layer - - row_group = col_group.row(align=True) - sub = row_group.column(align=True) - sub.prop_search(obj, "lightgroup", view_layer, "lightgroups", text="", results_are_suggestions=True) - - sub = row_group.column(align=True) - sub.enabled = bool(obj.lightgroup) and not any(lg.name == obj.lightgroup for lg in view_layer.lightgroups) - sub.operator("scene.view_layer_add_lightgroup", icon='ADD', text="").name = obj.lightgroup - - else: - # Original layout for other modes, with a shorter name field - col_name = row.column(align=True) - col_name.scale_x = 0.4 # Make the name field shorter - col_name.prop(obj, "name", text="") - - col_color = row.column(align=True) - col_color.scale_x = 0.25 - col_color.prop(light, "color", text="") - col_energy = row.column(align=True) - col_energy.scale_x = 0.35 - col_energy.prop(light, "energy", text="") - -# class DataButtonsPanel: -# bl_space_type = 'PROPERTIES' -# bl_region_type = 'WINDOW' -# bl_context = "data" - -# @classmethod -# def poll(cls, context): -# engine = context.engine -# return context.light and (engine in cls.COMPAT_ENGINES) + col_name = row.column(align=True) + col_name.scale_x = 0.4 + col_name.prop(obj, "name", text="") + col_color = row.column(align=True) + col_color.scale_x = 0.25 + col_color.prop(light, "color", text="") + col_energy = row.column(align=True) + col_energy.scale_x = 0.35 + col_energy.prop(light, "energy", text="") # ------------------------------------------------------------------------- -# 5) The main panel +# Main panel # ------------------------------------------------------------------------- class LIGHT_PT_editor(bpy.types.Panel): """Panel to view/edit lights with grouping, filtering, and per‐group toggles.""" @@ -670,269 +482,192 @@ class LIGHT_PT_editor(bpy.types.Panel): bl_space_type = "VIEW_3D" bl_region_type = "UI" bl_category = "Light Editor" - def draw(self, context): layout = self.layout scene = context.scene - layout.row().prop(scene, "filter_light_types", expand=True) layout.use_property_split = True - layout.use_property_decorate = False # No animation. - - - # Top toggles with new labels. - # row = layout.row(align=True) - # row.prop(scene, "light_editor_kind_alpha", text="By Kind") - # row.prop(scene, "light_editor_group_by_collection", text="By Collections") - # row.prop(scene, "light_editor_light_group", text="By Light Groups") - - + layout.use_property_decorate = False row = layout.row(align=True) row.prop(scene, "light_editor_filter", text="", icon="VIEWZOOM") row.operator("le.clear_light_filter", text="", icon='PANEL_CLOSE') - # Only display the render layer dropdown when in Collection mode. + # Render layer dropdown in COLLECTION mode: if scene.filter_light_types == 'COLLECTION': row = layout.row() row.prop(scene, "selected_render_layer", text="Render Layer") - - # Only show these rows if in Light Group mode. - if scene.filter_light_types == 'GROUP': - # # Row for adding a new light group. - # row = layout.row(align=True) - # row.prop(scene, "create_new_lightgroup", text="") - # row.operator("light_editor.create_new_lightgroup", text="Add Lightgroup") - - # # Row for selecting and deleting a light group. - # row = layout.row(align=True) - # row.prop(scene, "light_editor_delete_group", text="") - # row.operator("light_editor.delete_light_group", text="Delete Light Group") - - # copy from passes panel - view_layer = context.view_layer - - # layout.label(text="Light Groups") - # layout.alignment = 'CENTER' - row = layout.row() - col = row.column() - col.template_list("UI_UL_list", "lightgroups", view_layer, - "lightgroups", view_layer, "active_lightgroup_index", rows=3) - - col = row.column() - sub = col.column(align=True) - sub.operator("scene.view_layer_add_lightgroup", icon='ADD', text="") - sub.operator("scene.view_layer_remove_lightgroup", icon='REMOVE', text="") - sub.separator() - sub.menu("VIEWLAYER_MT_lightgroup_sync", icon='DOWNARROW_HLT', text="") - - - # Get lights from the active view layer - if scene.light_editor_filter: - lights = [obj for obj in context.view_layer.objects - if obj.type == 'LIGHT' and re.search(scene.light_editor_filter.lower(), obj.name.lower(), re.I)] - # if obj.type == 'LIGHT' and fnmatch.fnmatch(obj.name.lower(), scene.light_editor_filter.lower())] - else: - lights = [obj for obj in context.view_layer.objects if obj.type == 'LIGHT'] - + + # COLLECTION mode: group by collection if scene.filter_light_types == 'COLLECTION': - # Group by collection logic - groups = {} - for obj in lights: - if obj.users_collection: - group_name = obj.users_collection[0].name - else: - group_name = "No Collection" - groups.setdefault(group_name, []).append(obj) - for group_name, group_objs in groups.items(): - group_key = f"coll_{group_name}" - collapsed = group_collapse_dict.get(group_key, False) - header_box = layout.box() - header_row = header_box.row(align=True) - is_on_1 = group_checkbox_1_state.get(group_key, True) - icon_1 = 'CHECKBOX_HLT' if is_on_1 else 'CHECKBOX_DEHLT' - header_row.active = is_on_1 == True - op_1 = header_row.operator("light_editor.toggle_group_all_off", - text="", - icon=icon_1, - depress=is_on_1) - op_1.group_key = group_key - is_on_2 = group_checkbox_2_state.get(group_key, False) - icon_2 = 'RADIOBUT_ON' if is_on_2 else 'RADIOBUT_OFF' - op_2 = header_row.operator("light_editor.toggle_group_exclusive", - text="", - icon=icon_2, - depress=is_on_2) - op_2.group_key = group_key - op_tri = header_row.operator("light_editor.toggle_group", - text="", - emboss=True, - icon='RIGHTARROW' if collapsed else 'DOWNARROW_HLT') - op_tri.group_key = group_key - header_row.label(text=group_name, icon='OUTLINER_COLLECTION') - - if not collapsed and not scene.filter_light_types == 'GROUP': - for obj in group_objs: - draw_main_row(header_box, obj) - if obj.light_expanded: - extra_box = header_box.box() - # extra_box.label(text="Extra Parameters:") - draw_extra_params(self, extra_box, obj, obj.data) - - elif scene.filter_light_types == 'KIND': - # Group by kind logic - groups = {'POINT': [], 'SPOT': [], 'SUN': [], 'AREA': []} - for obj in lights: - if obj.data.type in groups: - groups[obj.data.type].append(obj) - for kind in ('POINT', 'SPOT', 'SUN', 'AREA'): - if groups[kind]: - group_key = f"kind_{kind}" + all_layer_colls = [] + gather_layer_collections(context.view_layer.layer_collection, all_layer_colls) + relevant_layer_colls = [] + for lc in all_layer_colls: + coll = lc.collection + if coll.name == "Scene Collection": + continue + if any(obj.type == 'LIGHT' for obj in coll.all_objects): + relevant_layer_colls.append(lc) + if not relevant_layer_colls: + box = layout.box() + box.label(text="No Collections Found", icon='ERROR') + else: + for lc in relevant_layer_colls: + coll = lc.collection + coll_name = coll.name + group_key = f"coll_{coll_name}" collapsed = group_collapse_dict.get(group_key, False) header_box = layout.box() header_row = header_box.row(align=True) - is_on_1 = group_checkbox_1_state.get(group_key, True) - icon_1 = 'CHECKBOX_HLT' if is_on_1 else 'CHECKBOX_DEHLT' - header_row.active = is_on_1 == True - op_1 = header_row.operator("light_editor.toggle_group_all_off", + is_included = not lc.exclude + icon_1 = 'CHECKBOX_HLT' if is_included else 'CHECKBOX_DEHLT' + header_row.active = True + op_1 = header_row.operator("light_editor.toggle_collection", text="", icon=icon_1, - depress=is_on_1) + depress=is_included) op_1.group_key = group_key + # Exclusive toggle button: icon is RADIOBUT_ON when active, RECORD_OFF when off. is_on_2 = group_checkbox_2_state.get(group_key, False) - icon_2 = 'RADIOBUT_ON' if is_on_2 else 'RADIOBUT_OFF' - op_2 = header_row.operator("light_editor.toggle_group_exclusive", - text="", - icon=icon_2, - depress=is_on_2) - op_2.group_key = group_key + icon_2 = 'RADIOBUT_ON' if is_on_2 else 'RECORD_OFF' + op_excl = header_row.operator("light_editor.toggle_group_exclusive", + text="", + icon=icon_2, + depress=is_on_2) + op_excl.group_key = group_key op_tri = header_row.operator("light_editor.toggle_group", text="", emboss=True, icon='RIGHTARROW' if collapsed else 'DOWNARROW_HLT') op_tri.group_key = group_key - header_row.label(text=f"{kind} Lights", icon=f"LIGHT_{kind}") + header_row.label(text=coll_name, icon='OUTLINER_COLLECTION') if not collapsed: - for obj in groups[kind]: + lights_in_coll = [o for o in coll.all_objects if o.type == 'LIGHT'] + filter_str = scene.light_editor_filter.strip() + if filter_str: + lights_in_coll = [o for o in lights_in_coll if re.search(filter_str.lower(), o.name.lower())] + for obj in lights_in_coll: draw_main_row(header_box, obj) if obj.light_expanded: extra_box = header_box.box() - # extra_box.label(text="Extra Parameters:") draw_extra_params(self, extra_box, obj, obj.data) - - elif scene.filter_light_types == 'GROUP': - groups = {} + + # KIND mode: group by kind + elif scene.filter_light_types == 'KIND': + if scene.light_editor_filter: + lights = [obj for obj in context.view_layer.objects if obj.type == 'LIGHT' and re.search(scene.light_editor_filter.lower(), obj.name.lower(), re.I)] + else: + lights = [obj for obj in context.view_layer.objects if obj.type == 'LIGHT'] + groups = {'POINT': [], 'SPOT': [], 'SUN': [], 'AREA': []} for obj in lights: - # Try the migrated property; fallback to old property if needed. - grp = getattr(obj, "lightgroup", "") - if not grp: - grp = getattr(obj, "lightgroup", "") - if not grp: - grp = "Not Assigned" - groups.setdefault(grp, []).append(obj) - for grp_name, group_objs in groups.items(): - group_key = f"group_{grp_name}" - collapsed = group_collapse_dict.get(group_key, False) - header_box = layout.box() - header_row = header_box.row(align=True) - is_on_1 = group_checkbox_1_state.get(group_key, True) - header_row.active = is_on_1 == True - icon_1 = 'CHECKBOX_HLT' if is_on_1 else 'CHECKBOX_DEHLT' - op_1 = header_row.operator("light_editor.toggle_group_all_off", - text="", - icon=icon_1, - depress=is_on_1) - op_1.group_key = group_key - is_on_2 = group_checkbox_2_state.get(group_key, False) - icon_2 = 'RADIOBUT_ON' if is_on_2 else 'RADIOBUT_OFF' - op_2 = header_row.operator("light_editor.toggle_group_exclusive", - text="", - icon=icon_2, - depress=is_on_2) - op_2.group_key = group_key - op_tri = header_row.operator("light_editor.toggle_group", - text="", - emboss=True, - icon='RIGHTARROW' if collapsed else 'DOWNARROW_HLT') - op_tri.group_key = group_key - header_row.label(text=f"{grp_name}", icon='GROUP') - if not collapsed: - for obj in group_objs: - draw_main_row(header_box, obj) - # We dont show extra light props in this mode - # if obj.light_expanded: - # extra_box = header_box.box() - # # extra_box.label(text="Extra Parameters:") - # draw_extra_params(self, extra_box, obj, obj.data) + if obj.data.type in groups: + groups[obj.data.type].append(obj) + if any(groups.values()): + for kind in ('POINT', 'SPOT', 'SUN', 'AREA'): + group_objs = groups[kind] + if group_objs: + group_key = f"kind_{kind}" + collapsed = group_collapse_dict.get(group_key, False) + header_box = layout.box() + header_row = header_box.row(align=True) + is_on_1 = group_checkbox_1_state.get(group_key, True) + icon_1 = 'CHECKBOX_HLT' if is_on_1 else 'CHECKBOX_DEHLT' + header_row.active = is_on_1 + op_1 = header_row.operator("light_editor.toggle_kind", + text="", + icon=icon_1, + depress=is_on_1) + op_1.group_key = group_key + # Exclusive toggle button for kind: icon is RADIOBUT_ON when active, RECORD_OFF when off. + is_on_2 = group_checkbox_2_state.get(group_key, False) + icon_2 = 'RADIOBUT_ON' if is_on_2 else 'RECORD_OFF' + op_excl_kind = header_row.operator("light_editor.toggle_group_exclusive", + text="", + icon=icon_2, + depress=is_on_2) + op_excl_kind.group_key = group_key + op_tri = header_row.operator("light_editor.toggle_group", + text="", + emboss=True, + icon='RIGHTARROW' if collapsed else 'DOWNARROW_HLT') + op_tri.group_key = group_key + header_row.label(text=f"{kind} Lights", icon=f"LIGHT_{kind}") + if not collapsed: + for obj in group_objs: + draw_main_row(header_box, obj) + if obj.light_expanded: + extra_box = header_box.box() + draw_extra_params(self, extra_box, obj, obj.data) + else: + box = layout.box() + box.label(text="No Lights Found", icon='ERROR') + + # ALPHABETICAL mode: list all lights sorted by name else: - # Alphabetical order mode - sorted_lights = sorted(lights, key=lambda o: o.name.lower()) - box = layout.box() - box.label(text="All Lights (Alphabetical)", icon='LIGHT_DATA') - for obj in sorted_lights: - draw_main_row(box, obj) - if obj.light_expanded: # Check if the light is expanded - extra_box = box.box() - # extra_box.label(text="Extra Parameters:") - draw_extra_params(self, extra_box, obj, obj.data) # Draw extra parameters - + if scene.light_editor_filter: + lights = [obj for obj in context.view_layer.objects if obj.type == 'LIGHT' and re.search(scene.light_editor_filter.lower(), obj.name.lower(), re.I)] + else: + lights = [obj for obj in context.view_layer.objects if obj.type == 'LIGHT'] + if lights: + box = layout.box() + box.label(text="All Lights (Alphabetical)", icon='LIGHT_DATA') + sorted_lights = sorted(lights, key=lambda o: o.name.lower()) + for obj in sorted_lights: + draw_main_row(box, obj) + if obj.light_expanded: + extra_box = box.box() + draw_extra_params(self, extra_box, obj, obj.data) + else: + box = layout.box() + box.label(text="No Lights Found", icon='ERROR') +# ------------------------------------------------------------------------- +# Handlers +# ------------------------------------------------------------------------- @persistent def LE_check_lights_enabled(dummy): for obj in bpy.data.objects: if obj.type == 'LIGHT': if (obj.hide_viewport and obj.hide_render): - bpy.context.view_layer.objects[obj.name].light_enabled = False + if obj.name in bpy.context.view_layer.objects: + bpy.context.view_layer.objects[obj.name].light_enabled = False else: - bpy.context.view_layer.objects[obj.name].light_enabled = True - + if obj.name in bpy.context.view_layer.objects: + bpy.context.view_layer.objects[obj.name].light_enabled = True @persistent def LE_clear_handler(dummy): context = bpy.context - # Migration: for each light, if it has an old "lightgroup" attribute, migrate it. - if bpy and bpy.data: - for obj in bpy.data.objects: - if obj.type == 'LIGHT': - old_group = getattr(obj, "lightgroup", "") - if old_group: - obj.lightgroup = old_group - # obj.custom_lightgroup_menu = obj.lightgroup if obj.lightgroup else "NoGroup" - # if old_group: - # obj.custom_lightgroup = old_group - # obj.custom_lightgroup_menu = obj.custom_lightgroup if obj.custom_lightgroup else "NoGroup" - - # Set enable correct - if (obj.hide_viewport == False and obj.hide_render == False): + for obj in bpy.data.objects: + if obj.type == 'LIGHT': + if not (obj.hide_viewport or obj.hide_render): + if obj.name in context.view_layer.objects: context.view_layer.objects[obj.name].light_enabled = True - else: + else: + if obj.name in context.view_layer.objects: context.view_layer.objects[obj.name].light_enabled = False - # self.hide_render = not self.light_enabled - +# ------------------------------------------------------------------------- +# Icon Loading +# ------------------------------------------------------------------------- def icon_Load(): - # importing icons import bpy.utils.previews global custom_icons custom_icons = bpy.utils.previews.new() - - # path to the folder where the icon is - # the path is calculated relative to this py file inside the addon folder icons_dir = os.path.join(os.path.dirname(__file__), 'icons') - - # load a preview thumbnail of a file and store in the previews collection custom_icons.load("SELECT_TRUE", os.path.join(icons_dir, "select_true.png"), 'IMAGE') custom_icons.load("SELECT_FALSE", os.path.join(icons_dir, "select_false.png"), 'IMAGE') -# global variable to store icons in custom_icons = None -# Register the new operator and property +# ------------------------------------------------------------------------- +# Registration +# ------------------------------------------------------------------------- classes = ( LIGHT_OT_ToggleGroup, - LIGHT_OT_ToggleGroupAllOff, + LIGHT_OT_ToggleCollection, + LIGHT_OT_ToggleKind, LIGHT_OT_ToggleGroupExclusive, - # LIGHT_OT_CreateNewLightgroup, - # LIGHT_OT_DeleteLightGroup, LIGHT_OT_ClearFilter, LIGHT_OT_SelectLight, LIGHT_PT_editor, @@ -940,90 +675,47 @@ def icon_Load(): def register(): icon_Load() - # Register scene properties + print("Light Editor add-on registered successfully.") bpy.types.Scene.current_active_light = bpy.props.PointerProperty(type=bpy.types.Object) bpy.types.Scene.current_exclusive_group = bpy.props.StringProperty() bpy.types.Scene.selected_render_layer = EnumProperty( name="Render Layer", description="Select the render layer", - items=get_render_layer_items, # Your function that lists view layers. + items=get_render_layer_items, update=update_render_layer ) - - # Register other classes and properties for cls in classes: bpy.utils.register_class(cls) - - # Register other properties (as shown in your original code) bpy.types.Scene.light_editor_filter = StringProperty( name="Filter", default="", - description="Filter lights by name (wildcards allowed)" + description="Filter lights by name (regex allowed)" ) bpy.types.Scene.light_editor_kind_alpha = BoolProperty( name="By Kind", description="Group lights by kind", - default=False, # Ensure this is False by default + default=False, update=update_group_by_kind ) bpy.types.Scene.light_editor_group_by_collection = BoolProperty( name="By Collections", description="Group lights by collection", - default=False, # Ensure this is False by default + default=False, update=update_group_by_collection ) - bpy.types.Scene.light_editor_light_group = BoolProperty( - name="By Light Groups", - description="Group lights by their custom light group assignment", - default=False, # Ensure this is False by default - update=update_light_group - ) - # bpy.types.Scene.create_new_lightgroup = StringProperty( - # name="", - # default="", - # description="Enter a new light group name" - # ) - bpy.types.Scene.custom_lightgroups_list = StringProperty( - name="Custom Lightgroups", - default="", - description="Comma separated list of custom light groups created without an active light" - ) - - # bpy.types.Scene.light_editor_delete_group = EnumProperty( - # name="Delete Light Group", - # description="Select a light group to delete", - # items=get_light_group_items # Pass the function directly (no parentheses) - # ) - bpy.types.Scene.filter_light_types = EnumProperty( name="Type", description="Filter light by type", - items=(('NO_FILTER', 'All', 'Show All no filter (Alphabetical)', 'NONE', 0), ('KIND', 'Kind', 'FIlter lights by Kind', 'LIGHT_DATA', 1),('COLLECTION', 'Collection', 'FIlter lights by Collections', 'OUTLINER_COLLECTION', 2),('GROUP', 'Light Group', 'Filter lights by Light Groups', 'GROUP', 3)), - update=update_light_types + items=(('NO_FILTER', 'All', 'Show All (Alphabetical)', 'NONE', 0), + ('KIND', 'Kind', 'Filter lights by Kind', 'LIGHT_DATA', 1), + ('COLLECTION', 'Collection', 'Filter lights by Collections', 'OUTLINER_COLLECTION', 2)), + default='NO_FILTER' ) - - # Register Light properties bpy.types.Light.soft_falloff = BoolProperty(default=False) bpy.types.Light.max_bounce = IntProperty(default=0, min=0, max=10) bpy.types.Light.multiple_instance = BoolProperty(default=False) bpy.types.Light.shadow_caustic = BoolProperty(default=False) bpy.types.Light.spread = FloatProperty(default=0.0, min=0.0, max=1.0) - - - # Register custom properties on Object using unique names - # bpy.types.Object.custom_lightgroup = StringProperty( - # name="Light Group", - # description="The custom lightgroup attribute for this object", - # default="" - # ) - - # bpy.types.Object.custom_lightgroup_menu = EnumProperty( - # name="Light Group", - # description="Select the custom light group for this light", - # items=get_light_group_items, # Pass the function directly (no parentheses) - # update=update_lightgroup_menu - # ) - bpy.types.Object.light_enabled = BoolProperty( name="Enabled", default=True, @@ -1036,52 +728,53 @@ def register(): ) bpy.types.Object.light_expanded = BoolProperty( name="Expanded", - default=False # Ensure this is False by default + default=False ) - - #add handler post load > see @persistent bpy.app.handlers.load_post.append(LE_clear_handler) - # bpy.app.handlers.depsgraph_update_post.append(LE_check_lights_enabled) bpy.app.handlers.load_post.append(LE_check_lights_enabled) - - - def unregister(): global custom_icons - bpy.utils.previews.remove(custom_icons) - - #removove handler post load > see @persistent - bpy.app.handlers.load_post.remove(LE_clear_handler) - bpy.app.handlers.load_post.remove(LE_check_lights_enabled) - - # Unregister scene properties - del bpy.types.Scene.current_active_light - del bpy.types.Scene.current_exclusive_group - del bpy.types.Scene.selected_render_layer - - # Unregister other classes and properties + if custom_icons: + bpy.utils.previews.remove(custom_icons) + custom_icons = None + if LE_clear_handler in bpy.app.handlers.load_post: + bpy.app.handlers.load_post.remove(LE_clear_handler) + if LE_check_lights_enabled in bpy.app.handlers.load_post: + bpy.app.handlers.load_post.remove(LE_check_lights_enabled) + if hasattr(bpy.types.Scene, 'current_active_light'): + del bpy.types.Scene.current_active_light + if hasattr(bpy.types.Scene, 'current_exclusive_group'): + del bpy.types.Scene.current_exclusive_group + if hasattr(bpy.types.Scene, 'selected_render_layer'): + del bpy.types.Scene.selected_render_layer + if hasattr(bpy.types.Scene, 'light_editor_filter'): + del bpy.types.Scene.light_editor_filter + if hasattr(bpy.types.Scene, 'light_editor_kind_alpha'): + del bpy.types.Scene.light_editor_kind_alpha + if hasattr(bpy.types.Scene, 'light_editor_group_by_collection'): + del bpy.types.Scene.light_editor_group_by_collection + if hasattr(bpy.types.Scene, 'filter_light_types'): + del bpy.types.Scene.filter_light_types + if hasattr(bpy.types.Light, 'soft_falloff'): + del bpy.types.Light.soft_falloff + if hasattr(bpy.types.Light, 'max_bounce'): + del bpy.types.Light.max_bounce + if hasattr(bpy.types.Light, 'multiple_instance'): + del bpy.types.Light.multiple_instance + if hasattr(bpy.types.Light, 'shadow_caustic'): + del bpy.types.Light.shadow_caustic + if hasattr(bpy.types.Light, 'spread'): + del bpy.types.Light.spread + if hasattr(bpy.types.Object, 'light_enabled'): + del bpy.types.Object.light_enabled + if hasattr(bpy.types.Object, 'light_turn_off_others'): + del bpy.types.Object.light_turn_off_others + if hasattr(bpy.types.Object, 'light_expanded'): + del bpy.types.Object.light_expanded for cls in reversed(classes): - bpy.utils.unregister_class(cls) - - # Unregister other properties (as shown in your original code) - del bpy.types.Scene.light_editor_filter - del bpy.types.Scene.light_editor_kind_alpha - del bpy.types.Scene.light_editor_group_by_collection - del bpy.types.Scene.light_editor_light_group - # del bpy.types.Scene.create_new_lightgroup - # del bpy.types.Scene.custom_lightgroups_list - del bpy.types.Scene.light_editor_delete_group - del bpy.types.Light.soft_falloff - del bpy.types.Light.max_bounce - del bpy.types.Light.multiple_instance - del bpy.types.Light.shadow_caustic - del bpy.types.Light.spread - # del bpy.types.Object.custom_lightgroup - # del bpy.types.Object.custom_lightgroup_menu - del bpy.types.Object.light_enabled - del bpy.types.Object.light_turn_off_others - del bpy.types.Object.light_expanded + if hasattr(bpy.types, cls.__name__): + bpy.utils.unregister_class(cls) if __name__ == "__main__": register() From 7ca738a15338a6c9cf5af67551f6f7d3a0d2b791 Mon Sep 17 00:00:00 2001 From: schroef Date: Wed, 19 Feb 2025 10:27:47 -0400 Subject: [PATCH 5/6] fix issues upstream and commit to main branch --- lightEditor.py | 1019 +++++++++++++++++------------------------------- 1 file changed, 356 insertions(+), 663 deletions(-) diff --git a/lightEditor.py b/lightEditor.py index c2a99c8..dfe09f4 100644 --- a/lightEditor.py +++ b/lightEditor.py @@ -15,93 +15,54 @@ current_active_light = None current_exclusive_group = None -# Dictionaries to store states -group_checkbox_1_state = {} # First button on/off per group_key (ON by default) +# Global dictionaries to track states +group_checkbox_1_state = {} # Used for first button (toggle group on/off) group_lights_original_state = {} # Stores original light states for each group -group_checkbox_2_state = {} # Second button on/off per group_key (OFF by default) -other_groups_original_state = {} # Stores other groups' states group_collapse_dict = {} # Whether each group is collapsed +collections_with_lights = {} # Tracks collections that contain lights -# ------------------------------------------------------------------------- -# Add a new operator for deleting a light group -# ------------------------------------------------------------------------- -class LIGHT_OT_DeleteLightGroup(bpy.types.Operator): - """Delete the selected light group.""" - bl_idname = "light_editor.delete_light_group" - bl_label = "Delete Light Group" - - def execute(self, context): - scene = context.scene - selected_group = scene.light_editor_delete_group - - if selected_group == "NoGroup": - self.report({'WARNING'}, "No group selected.") - return {'CANCELLED'} - - # Remove the group from all lights (use context.view_layer.objects) - for obj in context.view_layer.objects: - if obj.type == 'LIGHT' and getattr(obj, "custom_lightgroup", "") == selected_group: - obj.custom_lightgroup = "" - obj.custom_lightgroup_menu = "NoGroup" - - # Remove the group from the scene's custom_lightgroups_list - if hasattr(scene, "custom_lightgroups_list") and scene.custom_lightgroups_list: - groups = [g.strip() for g in scene.custom_lightgroups_list.split(",") if g.strip()] - if selected_group in groups: - groups.remove(selected_group) - scene.custom_lightgroups_list = ", ".join(groups) - - self.report({'INFO'}, f"Deleted light group '{selected_group}'") - return {'FINISHED'} +# New globals for the exclusive (second) button +group_checkbox_2_state = {} # Exclusive button state per group (False by default) +other_groups_original_state = {} # Stores the other groups' light states # ------------------------------------------------------------------------- -# 1) Functions for custom light group handling +# Render layer functions # ------------------------------------------------------------------------- -def get_light_groups(context): - """ - Loop through all objects, find lights, and collect them by their custom light group. - If a light already has an old "lightgroup" value, it is used. - Also include any groups stored in the scene property. - Returns a dictionary mapping group names to lists of light object names. - """ - groups = {} - for obj in context.view_layer.objects: - if obj.type == 'LIGHT': - # Try the new property; if empty, fallback to old property. - group_val = getattr(obj, "lightgroup", "") - if not group_val: - group_val = getattr(obj, "lightgroup", "") - if group_val: - groups.setdefault(group_val, []).append(obj.name) - # Get any groups created via the operator (even if no light has that assignment) - scene = context.scene - if hasattr(scene, "custom_lightgroups_list") and scene.custom_lightgroups_list: - extra_groups = [g.strip() for g in scene.custom_lightgroups_list.split(",") if g.strip()] - for g in extra_groups: - if g not in groups: - groups[g] = [] # no lights assigned yet - return groups - -def get_light_group_items(self, context): - """Return a list of custom light group items for the EnumProperty.""" - items = [("NoGroup", "No Group", "")] - groups = get_light_groups(context) # Pass context to get_light_groups - for group_name in groups.keys(): - if group_name != "NoGroup": - items.append((group_name, group_name, "")) - return items - -def update_lightgroup_menu(self, context): - """When the menu selection changes, copy the value into our custom_lightgroup property.""" - self.custom_lightgroup = self.custom_lightgroup_menu +def update_render_layer(self, context): + selected = self.selected_render_layer + for vl in context.scene.view_layers: + if vl.name == selected: + context.window.view_layer = vl + break -def view_layer_items(self, context): - """Return a list of view layer items for the EnumProperty.""" +def get_render_layer_items(self, context): items = [] for view_layer in context.scene.view_layers: items.append((view_layer.name, view_layer.name, "")) return items +# ------------------------------------------------------------------------- +# Collection functions +# ------------------------------------------------------------------------- +def gather_layer_collections(parent_lc, result): + """Recursively gather all LayerCollections in the parent_lc hierarchy.""" + result.append(parent_lc) + for child in parent_lc.children: + gather_layer_collections(child, result) + +def get_layer_collection_by_name(layer_collection, coll_name): + """Recursively search for a LayerCollection with the given name.""" + if layer_collection.collection.name == coll_name: + return layer_collection + for child in layer_collection.children: + found = get_layer_collection_by_name(child, coll_name) + if found: + return found + return None + +# ------------------------------------------------------------------------- +# Light update functions +# ------------------------------------------------------------------------- def update_light_enabled(self, context): self.hide_viewport = not self.light_enabled self.hide_render = not self.light_enabled @@ -112,7 +73,6 @@ def update_light_turn_off_others(self, context): if scene.current_active_light and scene.current_active_light != self: scene.current_active_light.light_turn_off_others = False scene.current_active_light = self - # Use objects from the active view layer for obj in context.view_layer.objects: if obj.type == 'LIGHT' and obj.name != self.name: if 'prev_light_enabled' not in obj: @@ -127,35 +87,35 @@ def update_light_turn_off_others(self, context): obj.light_enabled = obj['prev_light_enabled'] del obj['prev_light_enabled'] -# --- MUTUALLY EXCLUSIVE GROUPING TOGGLES --- +def get_all_collections(obj): + def _get_collections_recursive(collection, path=None): + if path is None: + path = [] + path.append(collection.name) + yield path[:] + for child in collection.children: + yield from _get_collections_recursive(child, path) + path.pop() + all_collections = set() + for collection in obj.users_collection: + for path in _get_collections_recursive(collection): + all_collections.add(" > ".join(path)) + return sorted(all_collections) + +# ------------------------------------------------------------------------- +# Mutually exclusive grouping toggles +# ------------------------------------------------------------------------- def update_group_by_kind(self, context): if self.light_editor_kind_alpha: self.light_editor_group_by_collection = False - self.light_editor_light_group = False def update_group_by_collection(self, context): if self.light_editor_group_by_collection: self.light_editor_kind_alpha = False - self.light_editor_light_group = False - -def update_light_group(self, context): - if self.light_editor_light_group: - self.light_editor_kind_alpha = False - self.light_editor_group_by_collection = False - - # Collapse all expanded lights when switching to "By Light Groups" mode - for obj in context.view_layer.objects: - if obj.type == 'LIGHT': - obj.light_expanded = False -def update_light_types(self, context): - if self == 'group': - # Collapse all expanded lights when switching to "By Light Groups" mode - for obj in context.view_layer.objects: - if obj.type == 'LIGHT': - obj.light_expanded = False - -# Used for MacOS +# ------------------------------------------------------------------------- +# Device functions (for cycles) +# ------------------------------------------------------------------------- def get_device_type(context): return context.preferences.addons['cycles'].preferences.compute_device_type @@ -164,11 +124,9 @@ def backend_has_active_gpu(context): def use_metal(context): cscene = context.scene.cycles - return (get_device_type(context) == 'METAL' and cscene.device == 'GPU' and backend_has_active_gpu(context)) def use_mnee(context): - # The MNEE kernel doesn't compile on macOS < 13. if use_metal(context): import platform version, _, _ = platform.mac_ver() @@ -178,21 +136,15 @@ def use_mnee(context): return True # ------------------------------------------------------------------------- -# 2) Extra parameters drawing function (unchanged) +# Extra parameters drawing function (unchanged) # ------------------------------------------------------------------------- def draw_extra_params(self, box, obj, light): - # Draw the default light properties UI in the provided box if light and isinstance(light, bpy.types.Light): - # Copy the layout drawing logic from DATA_PT_light layout = box - - # Display the 4 light type buttons in a single row row = layout.row() row.prop(light, "type", expand=True) - col = layout.column() col.separator() - if bpy.context.engine == 'CYCLES': clamp = light.cycles if light.type in {'POINT', 'SPOT'}: @@ -203,29 +155,23 @@ def draw_extra_params(self, box, obj, light): elif light.type == 'AREA': col.prop(light, "shape", text="Shape") sub = col.column(align=True) - if light.shape in {'SQUARE', 'DISK'}: sub.prop(light, "size") elif light.shape in {'RECTANGLE', 'ELLIPSE'}: sub.prop(light, "size", text="Size X") sub.prop(light, "size_y", text="Y") - if not (light.type == 'AREA' and clamp.is_portal): col.separator() sub = col.column() sub.prop(clamp, "max_bounces") - sub = col.column(align=True) sub.active = not (light.type == 'AREA' and clamp.is_portal) - sub.prop(light, "use_shadow", text="Cast Shadow") sub.prop(clamp, "use_multiple_importance_sampling", text="Multiple Importance") if use_mnee(bpy.context): sub.prop(clamp, "is_caustics_light", text="Shadow Caustics") - if light.type == 'AREA': col.prop(clamp, "is_portal", text="Portal") - if light.type == 'SPOT': col.separator() row = col.row(align=True) @@ -234,15 +180,12 @@ def draw_extra_params(self, box, obj, light): col.prop(light, "spot_size", text="Spot Size") col.prop(light, "spot_blend", text="Blend", slider=True) col.prop(light, "show_cone") - elif light.type == 'AREA': col.separator() row = col.row(align=True) row.alignment = 'CENTER' row.label(text="Beam Shape") col.prop(light, "spread", text="Spread") - - # EEVEE if ((bpy.context.engine == 'BLENDER_EEVEE') or (bpy.context.engine == 'BLENDER_EEVEE_NEXT')): col.separator() if light.type in {'POINT', 'SPOT'}: @@ -252,15 +195,12 @@ def draw_extra_params(self, box, obj, light): col.prop(light, "angle") elif light.type == 'AREA': col.prop(light, "shape") - sub = col.column(align=True) - if light.shape in {'SQUARE', 'DISK'}: sub.prop(light, "size") elif light.shape in {'RECTANGLE', 'ELLIPSE'}: sub.prop(light, "size", text="Size X") sub.prop(light, "size_y", text="Y") - if bpy.context.engine == 'BLENDER_EEVEE_NEXT': col.separator() col.prop(light, "use_shadow", text="Cast Shadow") @@ -268,8 +208,7 @@ def draw_extra_params(self, box, obj, light): col.prop(light, "shadow_jitter_overblur", text="Overblur") col.prop(light, "shadow_filter_radius", text="Radius") col.prop(light, "shadow_maximum_resolution", text="Resolution Limit") - - if (light and light.type == 'SPOT'): + if light and light.type == 'SPOT': col.separator() row = col.row(align=True) row.alignment = 'CENTER' @@ -277,80 +216,25 @@ def draw_extra_params(self, box, obj, light): col.prop(light, "spot_size", text="Size") col.prop(light, "spot_blend", text="Blend", slider=True) col.prop(light, "show_cone") - col.separator() col.prop(light, "diffuse_factor", text="Diffuse") col.prop(light, "specular_factor", text="Specular") col.prop(light, "volume_factor", text="Volume", text_ctxt=i18n_contexts.id_id) - - if (light and light.type != 'SUN'): + if light.type != 'SUN': col.separator() sub = col.column() sub.prop(light, "use_custom_distance", text="Custom Distance") sub.active = light.use_custom_distance sub.prop(light, "cutoff_distance", text="Distance") -# ------------------------------------------------------------------------- -# Render layer menu -# ------------------------------------------------------------------------- - -def get_render_layer_items(self, context): - """Return a list of render layer items for the EnumProperty.""" - items = [] - for view_layer in context.scene.view_layers: - items.append((view_layer.name, view_layer.name, "")) - return items - -def update_render_layer(self, context): - selected = self.selected_render_layer - # Iterate over the scene’s view layers: - for vl in context.scene.view_layers: - if vl.name == selected: - context.window.view_layer = vl - break - - -# ------------------------------------------------------------------------- -# 3) Operator: Add Lightgroup -# This operator now works even if no light is selected. -# When executed it assigns the new group name to the active light (if one exists) -# or adds the group name to the scene's custom_lightgroups_list. -# ------------------------------------------------------------------------- -class LIGHT_OT_CreateNewLightgroup(bpy.types.Operator): - """Add a new lightgroup and assign it (if a light is selected) or store it for later use.""" - bl_idname = "light_editor.create_new_lightgroup" - bl_label = "Add Lightgroup" - - def execute(self, context): - scene = context.scene - new_group = scene.create_new_lightgroup.strip() - if not new_group: - self.report({'WARNING'}, "No group name entered.") - return {'CANCELLED'} - active_obj = context.active_object - if active_obj and active_obj.type == 'LIGHT': - active_obj.custom_lightgroup = new_group - active_obj.custom_lightgroup_menu = new_group - self.report({'INFO'}, f"Assigned group '{new_group}' to {active_obj.name}") - else: - # No active light: add the group name to the scene list (if not already present) - current_list = scene.custom_lightgroups_list - groups = [g.strip() for g in current_list.split(",") if g.strip()] if current_list else [] - if new_group not in groups: - groups.append(new_group) - scene.custom_lightgroups_list = ", ".join(groups) - self.report({'INFO'}, f"Created light group '{new_group}'") - scene.create_new_lightgroup = "" - return {'FINISHED'} # ------------------------------------------------------------------------- -# 4) Operators for group toggling (object access updated) +# Operators # ------------------------------------------------------------------------- class LIGHT_OT_ToggleGroup(bpy.types.Operator): """Collapse or expand a group header.""" bl_idname = "light_editor.toggle_group" bl_label = "Toggle Group" - group_key: StringProperty() - + group_key: bpy.props.StringProperty() def execute(self, context): current = group_collapse_dict.get(self.group_key, False) group_collapse_dict[self.group_key] = not current @@ -359,12 +243,42 @@ def execute(self, context): area.tag_redraw() return {'FINISHED'} -class LIGHT_OT_ToggleGroupAllOff(bpy.types.Operator): - """Toggle all lights in the group off or restore them.""" - bl_idname = "light_editor.toggle_group_all_off" - bl_label = "Toggle Group All Off" - group_key: StringProperty() +class LIGHT_OT_ToggleCollection(bpy.types.Operator): + """Toggle the exclusion state of a collection (and its sub-collections).""" + bl_idname = "light_editor.toggle_collection" + bl_label = "Toggle Collection Exclusion" + group_key: bpy.props.StringProperty() + def execute(self, context): + global collections_with_lights + if self.group_key.startswith("coll_"): + coll_name = self.group_key[5:] + collection = bpy.data.collections.get(coll_name) + if not collection: + return {'CANCELLED'} + def toggle_exclusion_recursive(layer_coll, exclude): + layer_coll.exclude = exclude + for child in layer_coll.children: + toggle_exclusion_recursive(child, exclude) + layer_collection = get_layer_collection_by_name(context.view_layer.layer_collection, coll_name) + if layer_collection: + current_excluded = layer_collection.exclude + toggle_exclusion_recursive(layer_collection, not current_excluded) + def has_lights(coll): + return any(obj.type == 'LIGHT' for obj in coll.all_objects) + if has_lights(collection): + collections_with_lights[coll_name] = True + else: + collections_with_lights.pop(coll_name, None) + for area in context.screen.areas: + if area.type == 'VIEW_3D': + area.tag_redraw() + return {'FINISHED'} +class LIGHT_OT_ToggleKind(bpy.types.Operator): + """Toggle all lights of a specific kind.""" + bl_idname = "light_editor.toggle_kind" + bl_label = "Toggle Kind Visibility" + group_key: bpy.props.StringProperty() def execute(self, context): global group_checkbox_1_state, group_lights_original_state is_on = group_checkbox_1_state.get(self.group_key, True) @@ -372,227 +286,161 @@ def execute(self, context): if is_on: original_states = {} for obj in group_objs: - original_states[obj.name] = obj.light_enabled + if obj.type == 'LIGHT': + original_states[obj.name] = obj.light_enabled + obj.light_enabled = False group_lights_original_state[self.group_key] = original_states - for obj in group_objs: - obj.light_enabled = False - group_checkbox_1_state[self.group_key] = False else: original_states = group_lights_original_state.get(self.group_key, {}) for obj in group_objs: - obj.light_enabled = original_states.get(obj.name, True) + if obj.type == 'LIGHT': + obj.light_enabled = original_states.get(obj.name, True) if self.group_key in group_lights_original_state: del group_lights_original_state[self.group_key] - group_checkbox_1_state[self.group_key] = True + group_checkbox_1_state[self.group_key] = not is_on for area in context.screen.areas: if area.type == 'VIEW_3D': area.tag_redraw() return {'FINISHED'} - def _get_group_objects(self, context, group_key): - scene = context.scene - filter_pattern = scene.light_editor_filter.lower() + filter_pattern = context.scene.light_editor_filter.lower() + all_lights = [obj for obj in context.view_layer.objects if obj.type == 'LIGHT'] if filter_pattern: - # all_lights = [obj for obj in context.view_layer.objects - # if obj.type == 'LIGHT' and fnmatch.fnmatch(obj.name.lower(), filter_pattern)] - - all_lights = [obj for obj in context.view_layer.objects - if obj.type == 'LIGHT' and re.search(filter_pattern, obj.name, re.I)] - - # for fn in sorted(files): - # if not fn.lower().startswith("._"): - # if fn.lower().endswith(".hdr") or fn.lower().endswith(".exr") or fn.lower().endswith(".jpg") or fn.lower().endswith(".png"): - # if not scn.easyhdr_filter or re.search(scn.easyhdr_filter, fn, re.I): - # hdris.append(os.path.join(root, fn).replace(dir, '')) - # no_match = False - else: - all_lights = [obj for obj in context.view_layer.objects if obj.type == 'LIGHT'] - if scene.filter_light_types == 'COLLECTION' and group_key.startswith("coll_"): - coll_name = group_key[5:] - return [obj for obj in all_lights - if (obj.users_collection and obj.users_collection[0].name == coll_name) - or (not obj.users_collection and coll_name == "No Collection")] - if scene.filter_light_types == 'KIND' and group_key.startswith("kind_"): - kind = group_key[5:] - return [obj for obj in all_lights if obj.data.type == kind] - if scene.filter_light_types == 'GROUP' and group_key.startswith("group_"): - group_name = group_key[6:] - return [obj for obj in all_lights if getattr(obj, "custom_lightgroup", "") == group_name] - return [] + all_lights = [obj for obj in all_lights if re.search(filter_pattern, obj.name, re.I)] + kind = group_key[5:] + return [obj for obj in all_lights if obj.data.type == kind] class LIGHT_OT_ToggleGroupExclusive(bpy.types.Operator): """ Toggle exclusive mode for a group. - When turned on, lights in other groups (including "Not Assigned") are turned off. - When turned off, lights in other groups are restored to their previous states. + When turned on, lights in all other groups are turned off. + When turned off, the previous states of the other groups are restored. """ bl_idname = "light_editor.toggle_group_exclusive" bl_label = "Toggle Group Exclusive" - group_key: StringProperty() - + group_key: bpy.props.StringProperty() def execute(self, context): global current_exclusive_group, group_checkbox_2_state, other_groups_original_state - - # Check if the current group is already in exclusive mode is_on = group_checkbox_2_state.get(self.group_key, False) - if not is_on: - # If another group is already in exclusive mode, turn it off first if current_exclusive_group and current_exclusive_group != self.group_key: + saved = other_groups_original_state.get(current_exclusive_group, {}) + for gk, states in saved.items(): + objs = self._get_group_objects(context, gk) + for obj in objs: + obj.light_enabled = states.get(obj.name, True) group_checkbox_2_state[current_exclusive_group] = False - saved_dict = other_groups_original_state.get(current_exclusive_group, {}) - for gk, light_dict in saved_dict.items(): - grp_objs = self._get_group_objects(context, gk) - for obj in grp_objs: - obj.light_enabled = light_dict.get(obj.name, True) if current_exclusive_group in other_groups_original_state: del other_groups_original_state[current_exclusive_group] - - # Turn off all other groups (including "Not Assigned") and store their current states others = self._get_all_other_groups(context, self.group_key) - saved_dict = {} + saved = {} for gk in others: - grp_objs = self._get_group_objects(context, gk) - saved_dict[gk] = {obj.name: obj.light_enabled for obj in grp_objs} - for obj in grp_objs: + objs = self._get_group_objects(context, gk) + saved[gk] = {obj.name: obj.light_enabled for obj in objs} + for obj in objs: obj.light_enabled = False - - # Store the saved states and mark the current group as exclusive - other_groups_original_state[self.group_key] = saved_dict + other_groups_original_state[self.group_key] = saved group_checkbox_2_state[self.group_key] = True current_exclusive_group = self.group_key - else: - # Restore the states of all other groups (including "Not Assigned") - saved_dict = other_groups_original_state.get(self.group_key, {}) - for gk, light_dict in saved_dict.items(): - grp_objs = self._get_group_objects(context, gk) - for obj in grp_objs: - obj.light_enabled = light_dict.get(obj.name, True) - - # Clear the saved states and mark the current group as non-exclusive + saved = other_groups_original_state.get(self.group_key, {}) + for gk, states in saved.items(): + objs = self._get_group_objects(context, gk) + for obj in objs: + obj.light_enabled = states.get(obj.name, True) if self.group_key in other_groups_original_state: del other_groups_original_state[self.group_key] group_checkbox_2_state[self.group_key] = False current_exclusive_group = None - - # Redraw the UI to reflect the changes for area in context.screen.areas: if area.type == 'VIEW_3D': area.tag_redraw() - return {'FINISHED'} - def _get_all_other_groups(self, context, current_key): - """Get all group keys except the current one, including "Not Assigned".""" all_groups = self._get_all_group_keys(context) return [k for k in all_groups if k != current_key] - def _get_all_group_keys(self, context): - """Get all group keys, including "Not Assigned".""" scene = context.scene - filter_pattern = scene.light_editor_filter.lower() - if filter_pattern: - lights = [obj for obj in context.view_layer.objects - if obj.type == 'LIGHT' and re.search(filter_pattern, obj.name, re.I)] - else: - lights = [obj for obj in context.view_layer.objects if obj.type == 'LIGHT'] - - group_keys = set() - if scene.filter_light_types == 'COLLECTION': - for obj in lights: - if obj.users_collection: - coll_name = obj.users_collection[0].name - else: - coll_name = "No Collection" - group_keys.add(f"coll_{coll_name}") - elif scene.filter_light_types == 'KIND': - for obj in lights: - if obj.data.type in {'POINT', 'SPOT', 'SUN', 'AREA'}: - group_keys.add(f"kind_{obj.data.type}") - elif scene.filter_light_types == 'GROUP': - for obj in lights: - group_name = getattr(obj, "lightgroup", "") - if not group_name: - group_name = "Not Assigned" # Explicitly handle "Not Assigned" - group_keys.add(f"group_{group_name}") - return list(group_keys) - + filter_type = scene.filter_light_types + keys = set() + if filter_type == 'COLLECTION': + for obj in context.view_layer.objects: + if obj.type == 'LIGHT': + if obj.users_collection: + keys.add("coll_" + obj.users_collection[0].name) + else: + keys.add("coll_No Collection") + elif filter_type == 'KIND': + for obj in context.view_layer.objects: + if obj.type == 'LIGHT': + keys.add("kind_" + obj.data.type) + elif filter_type == 'GROUP': + for obj in context.view_layer.objects: + if obj.type == 'LIGHT': + grp = getattr(obj, "lightgroup", "") + if not grp: + grp = "Not Assigned" + keys.add("group_" + grp) + return list(keys) def _get_group_objects(self, context, group_key): - """Get all objects in the specified group, including "Not Assigned".""" scene = context.scene filter_pattern = scene.light_editor_filter.lower() + all_lights = [obj for obj in context.view_layer.objects if obj.type == 'LIGHT'] if filter_pattern: - all_lights = [obj for obj in context.view_layer.objects - if obj.type == 'LIGHT' and re.search(filter_pattern, obj.name, re.I)] - else: - all_lights = [obj for obj in context.view_layer.objects if obj.type == 'LIGHT'] - + all_lights = [obj for obj in all_lights if re.search(filter_pattern, obj.name, re.I)] if scene.filter_light_types == 'COLLECTION' and group_key.startswith("coll_"): coll_name = group_key[5:] - return [obj for obj in all_lights - if (obj.users_collection and obj.users_collection[0].name == coll_name) - or (not obj.users_collection and coll_name == "No Collection")] - if scene.filter_light_types == 'KIND' and group_key.startswith("kind_"): + return [obj for obj in all_lights if (obj.users_collection and obj.users_collection[0].name == coll_name) or (not obj.users_collection and coll_name == "No Collection")] + elif scene.filter_light_types == 'KIND' and group_key.startswith("kind_"): kind = group_key[5:] return [obj for obj in all_lights if obj.data.type == kind] - if scene.filter_light_types == 'GROUP' and group_key.startswith("group_"): - group_name = group_key[6:] - if group_name == "Not Assigned": + elif scene.filter_light_types == 'GROUP' and group_key.startswith("group_"): + grp_name = group_key[6:] + if grp_name == "Not Assigned": return [obj for obj in all_lights if not getattr(obj, "lightgroup", "")] else: - return [obj for obj in all_lights if getattr(obj, "lightgroup", "") == group_name] + return [obj for obj in all_lights if getattr(obj, "lightgroup", "") == grp_name] return [] class LIGHT_OT_ClearFilter(bpy.types.Operator): """Clear Filter Types""" bl_idname = "le.clear_light_filter" bl_label = "Clear Filter" - @classmethod - def poll(cls, context): - return context.scene.light_editor_filter - + def description(cls, context, properties): + scene = context.scene + if scene.filter_light_types == 'COLLECTION': + return "Turn ON/OFF Collection" + elif scene.filter_light_types == 'KIND': + return "Turn ON/OFF All Lights of This Kind" + return "Toggle all lights in the group off or restore them" def execute(self, context): - if context.scene.light_editor_filter: - context.scene.light_editor_filter = "" + context.scene.light_editor_filter = "" return {'FINISHED'} - class LIGHT_OT_SelectLight(bpy.types.Operator): """Selects light from UI and deselects everything else""" bl_idname = "le.select_light" bl_label = "Select Light" - - name : StringProperty() # Name of the light to select - + name : StringProperty() def execute(self, context): - # Get the view layer objects vob = context.view_layer.objects - - # Check if the light is already selected if self.name in vob: light = vob[self.name] - if light.select_get(): # If the light is already selected - # Deselect everything + if light.select_get(): bpy.ops.object.select_all(action='DESELECT') self.report({'INFO'}, f"Deselected all objects") else: - # Deselect all objects first bpy.ops.object.select_all(action='DESELECT') - # Select the specified light light.select_set(True) - vob.active = light # Set the light as the active object + vob.active = light self.report({'INFO'}, f"Selected light: {self.name}") else: self.report({'ERROR'}, f"Light '{self.name}' not found") - return {'FINISHED'} - - # ------------------------------------------------------------------------- -# 4) The main row UI: use custom_lightgroup property when in Light Group mode +# Main row UI drawing function # ------------------------------------------------------------------------- def draw_main_row(box, obj): light = obj.data @@ -600,68 +448,32 @@ def draw_main_row(box, obj): scene = bpy.context.scene row = box.row(align=True) controls_row = row.row(align=True) - - # The enabled and "turn off others" toggles remain: controls_row.prop(obj, "light_enabled", text="", icon="OUTLINER_OB_LIGHT" if obj.light_enabled else "LIGHT_DATA") - controls_row.active = obj.light_enabled == True + controls_row.active = (obj.light_enabled == True) controls_row.prop(obj, "light_turn_off_others", text="", icon="RADIOBUT_ON" if obj.light_turn_off_others else "RADIOBUT_OFF") - if not scene.filter_light_types == 'GROUP': selected_true = custom_icons["SELECT_TRUE"].icon_id selected_false = custom_icons["SELECT_FALSE"].icon_id - controls_row.operator("le.select_light", text="", - icon_value=selected_true if obj.select_get() == True else selected_false).name = obj.name - - # -- Only show the triangle if NOT in Light Group mode -- + controls_row.operator("le.select_light", text="", + icon_value=selected_true if obj.select_get() else selected_false).name = obj.name if not scene.filter_light_types == 'GROUP': controls_row.prop(obj, "light_expanded", text="", emboss=True, icon='DOWNARROW_HLT' if obj.light_expanded else 'RIGHTARROW') - - # Apply 50-50 split only in Light Group mode - if scene.filter_light_types == 'GROUP': - split = row.split(factor=0.5) - col_name = split.column(align=True) - col_name.prop(obj, "name", text="") # Default width in Light Group mode - - col_group = split.column(align=True) - view_layer = context.view_layer - - row_group = col_group.row(align=True) - sub = row_group.column(align=True) - sub.prop_search(obj, "lightgroup", view_layer, "lightgroups", text="", results_are_suggestions=True) - - sub = row_group.column(align=True) - sub.enabled = bool(obj.lightgroup) and not any(lg.name == obj.lightgroup for lg in view_layer.lightgroups) - sub.operator("scene.view_layer_add_lightgroup", icon='ADD', text="").name = obj.lightgroup - - else: - # Original layout for other modes, with a shorter name field - col_name = row.column(align=True) - col_name.scale_x = 0.4 # Make the name field shorter - col_name.prop(obj, "name", text="") - - col_color = row.column(align=True) - col_color.scale_x = 0.25 - col_color.prop(light, "color", text="") - col_energy = row.column(align=True) - col_energy.scale_x = 0.35 - col_energy.prop(light, "energy", text="") - -# class DataButtonsPanel: -# bl_space_type = 'PROPERTIES' -# bl_region_type = 'WINDOW' -# bl_context = "data" - -# @classmethod -# def poll(cls, context): -# engine = context.engine -# return context.light and (engine in cls.COMPAT_ENGINES) + col_name = row.column(align=True) + col_name.scale_x = 0.4 + col_name.prop(obj, "name", text="") + col_color = row.column(align=True) + col_color.scale_x = 0.25 + col_color.prop(light, "color", text="") + col_energy = row.column(align=True) + col_energy.scale_x = 0.35 + col_energy.prop(light, "energy", text="") # ------------------------------------------------------------------------- -# 5) The main panel +# Main panel # ------------------------------------------------------------------------- class LIGHT_PT_editor(bpy.types.Panel): """Panel to view/edit lights with grouping, filtering, and per‐group toggles.""" @@ -670,269 +482,192 @@ class LIGHT_PT_editor(bpy.types.Panel): bl_space_type = "VIEW_3D" bl_region_type = "UI" bl_category = "Light Editor" - def draw(self, context): layout = self.layout scene = context.scene - layout.row().prop(scene, "filter_light_types", expand=True) layout.use_property_split = True - layout.use_property_decorate = False # No animation. - - - # Top toggles with new labels. - # row = layout.row(align=True) - # row.prop(scene, "light_editor_kind_alpha", text="By Kind") - # row.prop(scene, "light_editor_group_by_collection", text="By Collections") - # row.prop(scene, "light_editor_light_group", text="By Light Groups") - - + layout.use_property_decorate = False row = layout.row(align=True) row.prop(scene, "light_editor_filter", text="", icon="VIEWZOOM") row.operator("le.clear_light_filter", text="", icon='PANEL_CLOSE') - # Only display the render layer dropdown when in Collection mode. + # Render layer dropdown in COLLECTION mode: if scene.filter_light_types == 'COLLECTION': row = layout.row() row.prop(scene, "selected_render_layer", text="Render Layer") - - # Only show these rows if in Light Group mode. - if scene.filter_light_types == 'GROUP': - # # Row for adding a new light group. - # row = layout.row(align=True) - # row.prop(scene, "create_new_lightgroup", text="") - # row.operator("light_editor.create_new_lightgroup", text="Add Lightgroup") - - # # Row for selecting and deleting a light group. - # row = layout.row(align=True) - # row.prop(scene, "light_editor_delete_group", text="") - # row.operator("light_editor.delete_light_group", text="Delete Light Group") - - # copy from passes panel - view_layer = context.view_layer - - # layout.label(text="Light Groups") - # layout.alignment = 'CENTER' - row = layout.row() - col = row.column() - col.template_list("UI_UL_list", "lightgroups", view_layer, - "lightgroups", view_layer, "active_lightgroup_index", rows=3) - - col = row.column() - sub = col.column(align=True) - sub.operator("scene.view_layer_add_lightgroup", icon='ADD', text="") - sub.operator("scene.view_layer_remove_lightgroup", icon='REMOVE', text="") - sub.separator() - sub.menu("VIEWLAYER_MT_lightgroup_sync", icon='DOWNARROW_HLT', text="") - - - # Get lights from the active view layer - if scene.light_editor_filter: - lights = [obj for obj in context.view_layer.objects - if obj.type == 'LIGHT' and re.search(scene.light_editor_filter.lower(), obj.name.lower(), re.I)] - # if obj.type == 'LIGHT' and fnmatch.fnmatch(obj.name.lower(), scene.light_editor_filter.lower())] - else: - lights = [obj for obj in context.view_layer.objects if obj.type == 'LIGHT'] - + + # COLLECTION mode: group by collection if scene.filter_light_types == 'COLLECTION': - # Group by collection logic - groups = {} - for obj in lights: - if obj.users_collection: - group_name = obj.users_collection[0].name - else: - group_name = "No Collection" - groups.setdefault(group_name, []).append(obj) - for group_name, group_objs in groups.items(): - group_key = f"coll_{group_name}" - collapsed = group_collapse_dict.get(group_key, False) - header_box = layout.box() - header_row = header_box.row(align=True) - is_on_1 = group_checkbox_1_state.get(group_key, True) - icon_1 = 'CHECKBOX_HLT' if is_on_1 else 'CHECKBOX_DEHLT' - header_row.active = is_on_1 == True - op_1 = header_row.operator("light_editor.toggle_group_all_off", - text="", - icon=icon_1, - depress=is_on_1) - op_1.group_key = group_key - is_on_2 = group_checkbox_2_state.get(group_key, False) - icon_2 = 'RADIOBUT_ON' if is_on_2 else 'RADIOBUT_OFF' - op_2 = header_row.operator("light_editor.toggle_group_exclusive", - text="", - icon=icon_2, - depress=is_on_2) - op_2.group_key = group_key - op_tri = header_row.operator("light_editor.toggle_group", - text="", - emboss=True, - icon='RIGHTARROW' if collapsed else 'DOWNARROW_HLT') - op_tri.group_key = group_key - header_row.label(text=group_name, icon='OUTLINER_COLLECTION') - - if not collapsed and not scene.filter_light_types == 'GROUP': - for obj in group_objs: - draw_main_row(header_box, obj) - if obj.light_expanded: - extra_box = header_box.box() - # extra_box.label(text="Extra Parameters:") - draw_extra_params(self, extra_box, obj, obj.data) - - elif scene.filter_light_types == 'KIND': - # Group by kind logic - groups = {'POINT': [], 'SPOT': [], 'SUN': [], 'AREA': []} - for obj in lights: - if obj.data.type in groups: - groups[obj.data.type].append(obj) - for kind in ('POINT', 'SPOT', 'SUN', 'AREA'): - if groups[kind]: - group_key = f"kind_{kind}" + all_layer_colls = [] + gather_layer_collections(context.view_layer.layer_collection, all_layer_colls) + relevant_layer_colls = [] + for lc in all_layer_colls: + coll = lc.collection + if coll.name == "Scene Collection": + continue + if any(obj.type == 'LIGHT' for obj in coll.all_objects): + relevant_layer_colls.append(lc) + if not relevant_layer_colls: + box = layout.box() + box.label(text="No Collections Found", icon='ERROR') + else: + for lc in relevant_layer_colls: + coll = lc.collection + coll_name = coll.name + group_key = f"coll_{coll_name}" collapsed = group_collapse_dict.get(group_key, False) header_box = layout.box() header_row = header_box.row(align=True) - is_on_1 = group_checkbox_1_state.get(group_key, True) - icon_1 = 'CHECKBOX_HLT' if is_on_1 else 'CHECKBOX_DEHLT' - header_row.active = is_on_1 == True - op_1 = header_row.operator("light_editor.toggle_group_all_off", + is_included = not lc.exclude + icon_1 = 'CHECKBOX_HLT' if is_included else 'CHECKBOX_DEHLT' + header_row.active = True + op_1 = header_row.operator("light_editor.toggle_collection", text="", icon=icon_1, - depress=is_on_1) + depress=is_included) op_1.group_key = group_key + # Exclusive toggle button: icon is RADIOBUT_ON when active, RECORD_OFF when off. is_on_2 = group_checkbox_2_state.get(group_key, False) - icon_2 = 'RADIOBUT_ON' if is_on_2 else 'RADIOBUT_OFF' - op_2 = header_row.operator("light_editor.toggle_group_exclusive", - text="", - icon=icon_2, - depress=is_on_2) - op_2.group_key = group_key + icon_2 = 'RADIOBUT_ON' if is_on_2 else 'RECORD_OFF' + op_excl = header_row.operator("light_editor.toggle_group_exclusive", + text="", + icon=icon_2, + depress=is_on_2) + op_excl.group_key = group_key op_tri = header_row.operator("light_editor.toggle_group", text="", emboss=True, icon='RIGHTARROW' if collapsed else 'DOWNARROW_HLT') op_tri.group_key = group_key - header_row.label(text=f"{kind} Lights", icon=f"LIGHT_{kind}") + header_row.label(text=coll_name, icon='OUTLINER_COLLECTION') if not collapsed: - for obj in groups[kind]: + lights_in_coll = [o for o in coll.all_objects if o.type == 'LIGHT'] + filter_str = scene.light_editor_filter.strip() + if filter_str: + lights_in_coll = [o for o in lights_in_coll if re.search(filter_str.lower(), o.name.lower())] + for obj in lights_in_coll: draw_main_row(header_box, obj) if obj.light_expanded: extra_box = header_box.box() - # extra_box.label(text="Extra Parameters:") draw_extra_params(self, extra_box, obj, obj.data) - - elif scene.filter_light_types == 'GROUP': - groups = {} + + # KIND mode: group by kind + elif scene.filter_light_types == 'KIND': + if scene.light_editor_filter: + lights = [obj for obj in context.view_layer.objects if obj.type == 'LIGHT' and re.search(scene.light_editor_filter.lower(), obj.name.lower(), re.I)] + else: + lights = [obj for obj in context.view_layer.objects if obj.type == 'LIGHT'] + groups = {'POINT': [], 'SPOT': [], 'SUN': [], 'AREA': []} for obj in lights: - # Try the migrated property; fallback to old property if needed. - grp = getattr(obj, "lightgroup", "") - if not grp: - grp = getattr(obj, "lightgroup", "") - if not grp: - grp = "Not Assigned" - groups.setdefault(grp, []).append(obj) - for grp_name, group_objs in groups.items(): - group_key = f"group_{grp_name}" - collapsed = group_collapse_dict.get(group_key, False) - header_box = layout.box() - header_row = header_box.row(align=True) - is_on_1 = group_checkbox_1_state.get(group_key, True) - header_row.active = is_on_1 == True - icon_1 = 'CHECKBOX_HLT' if is_on_1 else 'CHECKBOX_DEHLT' - op_1 = header_row.operator("light_editor.toggle_group_all_off", - text="", - icon=icon_1, - depress=is_on_1) - op_1.group_key = group_key - is_on_2 = group_checkbox_2_state.get(group_key, False) - icon_2 = 'RADIOBUT_ON' if is_on_2 else 'RADIOBUT_OFF' - op_2 = header_row.operator("light_editor.toggle_group_exclusive", - text="", - icon=icon_2, - depress=is_on_2) - op_2.group_key = group_key - op_tri = header_row.operator("light_editor.toggle_group", - text="", - emboss=True, - icon='RIGHTARROW' if collapsed else 'DOWNARROW_HLT') - op_tri.group_key = group_key - header_row.label(text=f"{grp_name}", icon='GROUP') - if not collapsed: - for obj in group_objs: - draw_main_row(header_box, obj) - # We dont show extra light props in this mode - # if obj.light_expanded: - # extra_box = header_box.box() - # # extra_box.label(text="Extra Parameters:") - # draw_extra_params(self, extra_box, obj, obj.data) + if obj.data.type in groups: + groups[obj.data.type].append(obj) + if any(groups.values()): + for kind in ('POINT', 'SPOT', 'SUN', 'AREA'): + group_objs = groups[kind] + if group_objs: + group_key = f"kind_{kind}" + collapsed = group_collapse_dict.get(group_key, False) + header_box = layout.box() + header_row = header_box.row(align=True) + is_on_1 = group_checkbox_1_state.get(group_key, True) + icon_1 = 'CHECKBOX_HLT' if is_on_1 else 'CHECKBOX_DEHLT' + header_row.active = is_on_1 + op_1 = header_row.operator("light_editor.toggle_kind", + text="", + icon=icon_1, + depress=is_on_1) + op_1.group_key = group_key + # Exclusive toggle button for kind: icon is RADIOBUT_ON when active, RECORD_OFF when off. + is_on_2 = group_checkbox_2_state.get(group_key, False) + icon_2 = 'RADIOBUT_ON' if is_on_2 else 'RECORD_OFF' + op_excl_kind = header_row.operator("light_editor.toggle_group_exclusive", + text="", + icon=icon_2, + depress=is_on_2) + op_excl_kind.group_key = group_key + op_tri = header_row.operator("light_editor.toggle_group", + text="", + emboss=True, + icon='RIGHTARROW' if collapsed else 'DOWNARROW_HLT') + op_tri.group_key = group_key + header_row.label(text=f"{kind} Lights", icon=f"LIGHT_{kind}") + if not collapsed: + for obj in group_objs: + draw_main_row(header_box, obj) + if obj.light_expanded: + extra_box = header_box.box() + draw_extra_params(self, extra_box, obj, obj.data) + else: + box = layout.box() + box.label(text="No Lights Found", icon='ERROR') + + # ALPHABETICAL mode: list all lights sorted by name else: - # Alphabetical order mode - sorted_lights = sorted(lights, key=lambda o: o.name.lower()) - box = layout.box() - box.label(text="All Lights (Alphabetical)", icon='LIGHT_DATA') - for obj in sorted_lights: - draw_main_row(box, obj) - if obj.light_expanded: # Check if the light is expanded - extra_box = box.box() - # extra_box.label(text="Extra Parameters:") - draw_extra_params(self, extra_box, obj, obj.data) # Draw extra parameters - + if scene.light_editor_filter: + lights = [obj for obj in context.view_layer.objects if obj.type == 'LIGHT' and re.search(scene.light_editor_filter.lower(), obj.name.lower(), re.I)] + else: + lights = [obj for obj in context.view_layer.objects if obj.type == 'LIGHT'] + if lights: + box = layout.box() + box.label(text="All Lights (Alphabetical)", icon='LIGHT_DATA') + sorted_lights = sorted(lights, key=lambda o: o.name.lower()) + for obj in sorted_lights: + draw_main_row(box, obj) + if obj.light_expanded: + extra_box = box.box() + draw_extra_params(self, extra_box, obj, obj.data) + else: + box = layout.box() + box.label(text="No Lights Found", icon='ERROR') +# ------------------------------------------------------------------------- +# Handlers +# ------------------------------------------------------------------------- @persistent def LE_check_lights_enabled(dummy): for obj in bpy.data.objects: if obj.type == 'LIGHT': if (obj.hide_viewport and obj.hide_render): - bpy.context.view_layer.objects[obj.name].light_enabled = False + if obj.name in bpy.context.view_layer.objects: + bpy.context.view_layer.objects[obj.name].light_enabled = False else: - bpy.context.view_layer.objects[obj.name].light_enabled = True - + if obj.name in bpy.context.view_layer.objects: + bpy.context.view_layer.objects[obj.name].light_enabled = True @persistent def LE_clear_handler(dummy): context = bpy.context - # Migration: for each light, if it has an old "lightgroup" attribute, migrate it. - if bpy and bpy.data: - for obj in bpy.data.objects: - if obj.type == 'LIGHT': - old_group = getattr(obj, "lightgroup", "") - if old_group: - obj.lightgroup = old_group - # obj.custom_lightgroup_menu = obj.lightgroup if obj.lightgroup else "NoGroup" - # if old_group: - # obj.custom_lightgroup = old_group - # obj.custom_lightgroup_menu = obj.custom_lightgroup if obj.custom_lightgroup else "NoGroup" - - # Set enable correct - if (obj.hide_viewport == False and obj.hide_render == False): + for obj in bpy.data.objects: + if obj.type == 'LIGHT': + if not (obj.hide_viewport or obj.hide_render): + if obj.name in context.view_layer.objects: context.view_layer.objects[obj.name].light_enabled = True - else: + else: + if obj.name in context.view_layer.objects: context.view_layer.objects[obj.name].light_enabled = False - # self.hide_render = not self.light_enabled - +# ------------------------------------------------------------------------- +# Icon Loading +# ------------------------------------------------------------------------- def icon_Load(): - # importing icons import bpy.utils.previews global custom_icons custom_icons = bpy.utils.previews.new() - - # path to the folder where the icon is - # the path is calculated relative to this py file inside the addon folder icons_dir = os.path.join(os.path.dirname(__file__), 'icons') - - # load a preview thumbnail of a file and store in the previews collection custom_icons.load("SELECT_TRUE", os.path.join(icons_dir, "select_true.png"), 'IMAGE') custom_icons.load("SELECT_FALSE", os.path.join(icons_dir, "select_false.png"), 'IMAGE') -# global variable to store icons in custom_icons = None -# Register the new operator and property +# ------------------------------------------------------------------------- +# Registration +# ------------------------------------------------------------------------- classes = ( LIGHT_OT_ToggleGroup, - LIGHT_OT_ToggleGroupAllOff, + LIGHT_OT_ToggleCollection, + LIGHT_OT_ToggleKind, LIGHT_OT_ToggleGroupExclusive, - # LIGHT_OT_CreateNewLightgroup, - # LIGHT_OT_DeleteLightGroup, LIGHT_OT_ClearFilter, LIGHT_OT_SelectLight, LIGHT_PT_editor, @@ -940,90 +675,47 @@ def icon_Load(): def register(): icon_Load() - # Register scene properties + print("Light Editor add-on registered successfully.") bpy.types.Scene.current_active_light = bpy.props.PointerProperty(type=bpy.types.Object) bpy.types.Scene.current_exclusive_group = bpy.props.StringProperty() bpy.types.Scene.selected_render_layer = EnumProperty( name="Render Layer", description="Select the render layer", - items=get_render_layer_items, # Your function that lists view layers. + items=get_render_layer_items, update=update_render_layer ) - - # Register other classes and properties for cls in classes: bpy.utils.register_class(cls) - - # Register other properties (as shown in your original code) bpy.types.Scene.light_editor_filter = StringProperty( name="Filter", default="", - description="Filter lights by name (wildcards allowed)" + description="Filter lights by name (regex allowed)" ) bpy.types.Scene.light_editor_kind_alpha = BoolProperty( name="By Kind", description="Group lights by kind", - default=False, # Ensure this is False by default + default=False, update=update_group_by_kind ) bpy.types.Scene.light_editor_group_by_collection = BoolProperty( name="By Collections", description="Group lights by collection", - default=False, # Ensure this is False by default + default=False, update=update_group_by_collection ) - bpy.types.Scene.light_editor_light_group = BoolProperty( - name="By Light Groups", - description="Group lights by their custom light group assignment", - default=False, # Ensure this is False by default - update=update_light_group - ) - # bpy.types.Scene.create_new_lightgroup = StringProperty( - # name="", - # default="", - # description="Enter a new light group name" - # ) - bpy.types.Scene.custom_lightgroups_list = StringProperty( - name="Custom Lightgroups", - default="", - description="Comma separated list of custom light groups created without an active light" - ) - - # bpy.types.Scene.light_editor_delete_group = EnumProperty( - # name="Delete Light Group", - # description="Select a light group to delete", - # items=get_light_group_items # Pass the function directly (no parentheses) - # ) - bpy.types.Scene.filter_light_types = EnumProperty( name="Type", description="Filter light by type", - items=(('NO_FILTER', 'All', 'Show All no filter (Alphabetical)', 'NONE', 0), ('KIND', 'Kind', 'FIlter lights by Kind', 'LIGHT_DATA', 1),('COLLECTION', 'Collection', 'FIlter lights by Collections', 'OUTLINER_COLLECTION', 2),('GROUP', 'Light Group', 'Filter lights by Light Groups', 'GROUP', 3)), - update=update_light_types + items=(('NO_FILTER', 'All', 'Show All (Alphabetical)', 'NONE', 0), + ('KIND', 'Kind', 'Filter lights by Kind', 'LIGHT_DATA', 1), + ('COLLECTION', 'Collection', 'Filter lights by Collections', 'OUTLINER_COLLECTION', 2)), + default='NO_FILTER' ) - - # Register Light properties bpy.types.Light.soft_falloff = BoolProperty(default=False) bpy.types.Light.max_bounce = IntProperty(default=0, min=0, max=10) bpy.types.Light.multiple_instance = BoolProperty(default=False) bpy.types.Light.shadow_caustic = BoolProperty(default=False) bpy.types.Light.spread = FloatProperty(default=0.0, min=0.0, max=1.0) - - - # Register custom properties on Object using unique names - # bpy.types.Object.custom_lightgroup = StringProperty( - # name="Light Group", - # description="The custom lightgroup attribute for this object", - # default="" - # ) - - # bpy.types.Object.custom_lightgroup_menu = EnumProperty( - # name="Light Group", - # description="Select the custom light group for this light", - # items=get_light_group_items, # Pass the function directly (no parentheses) - # update=update_lightgroup_menu - # ) - bpy.types.Object.light_enabled = BoolProperty( name="Enabled", default=True, @@ -1036,52 +728,53 @@ def register(): ) bpy.types.Object.light_expanded = BoolProperty( name="Expanded", - default=False # Ensure this is False by default + default=False ) - - #add handler post load > see @persistent bpy.app.handlers.load_post.append(LE_clear_handler) - # bpy.app.handlers.depsgraph_update_post.append(LE_check_lights_enabled) bpy.app.handlers.load_post.append(LE_check_lights_enabled) - - - def unregister(): global custom_icons - bpy.utils.previews.remove(custom_icons) - - #removove handler post load > see @persistent - bpy.app.handlers.load_post.remove(LE_clear_handler) - bpy.app.handlers.load_post.remove(LE_check_lights_enabled) - - # Unregister scene properties - del bpy.types.Scene.current_active_light - del bpy.types.Scene.current_exclusive_group - del bpy.types.Scene.selected_render_layer - - # Unregister other classes and properties + if custom_icons: + bpy.utils.previews.remove(custom_icons) + custom_icons = None + if LE_clear_handler in bpy.app.handlers.load_post: + bpy.app.handlers.load_post.remove(LE_clear_handler) + if LE_check_lights_enabled in bpy.app.handlers.load_post: + bpy.app.handlers.load_post.remove(LE_check_lights_enabled) + if hasattr(bpy.types.Scene, 'current_active_light'): + del bpy.types.Scene.current_active_light + if hasattr(bpy.types.Scene, 'current_exclusive_group'): + del bpy.types.Scene.current_exclusive_group + if hasattr(bpy.types.Scene, 'selected_render_layer'): + del bpy.types.Scene.selected_render_layer + if hasattr(bpy.types.Scene, 'light_editor_filter'): + del bpy.types.Scene.light_editor_filter + if hasattr(bpy.types.Scene, 'light_editor_kind_alpha'): + del bpy.types.Scene.light_editor_kind_alpha + if hasattr(bpy.types.Scene, 'light_editor_group_by_collection'): + del bpy.types.Scene.light_editor_group_by_collection + if hasattr(bpy.types.Scene, 'filter_light_types'): + del bpy.types.Scene.filter_light_types + if hasattr(bpy.types.Light, 'soft_falloff'): + del bpy.types.Light.soft_falloff + if hasattr(bpy.types.Light, 'max_bounce'): + del bpy.types.Light.max_bounce + if hasattr(bpy.types.Light, 'multiple_instance'): + del bpy.types.Light.multiple_instance + if hasattr(bpy.types.Light, 'shadow_caustic'): + del bpy.types.Light.shadow_caustic + if hasattr(bpy.types.Light, 'spread'): + del bpy.types.Light.spread + if hasattr(bpy.types.Object, 'light_enabled'): + del bpy.types.Object.light_enabled + if hasattr(bpy.types.Object, 'light_turn_off_others'): + del bpy.types.Object.light_turn_off_others + if hasattr(bpy.types.Object, 'light_expanded'): + del bpy.types.Object.light_expanded for cls in reversed(classes): - bpy.utils.unregister_class(cls) - - # Unregister other properties (as shown in your original code) - del bpy.types.Scene.light_editor_filter - del bpy.types.Scene.light_editor_kind_alpha - del bpy.types.Scene.light_editor_group_by_collection - del bpy.types.Scene.light_editor_light_group - # del bpy.types.Scene.create_new_lightgroup - # del bpy.types.Scene.custom_lightgroups_list - del bpy.types.Scene.light_editor_delete_group - del bpy.types.Light.soft_falloff - del bpy.types.Light.max_bounce - del bpy.types.Light.multiple_instance - del bpy.types.Light.shadow_caustic - del bpy.types.Light.spread - # del bpy.types.Object.custom_lightgroup - # del bpy.types.Object.custom_lightgroup_menu - del bpy.types.Object.light_enabled - del bpy.types.Object.light_turn_off_others - del bpy.types.Object.light_expanded + if hasattr(bpy.types, cls.__name__): + bpy.utils.unregister_class(cls) if __name__ == "__main__": register() From 0ac0f0d2e9dc045fe2d75a73148a84aa9614da80 Mon Sep 17 00:00:00 2001 From: Rombout Versluijs Date: Fri, 15 Aug 2025 16:59:11 -0400 Subject: [PATCH 6/6] Added Specials Menu - this adds a special menu dropdown with 4 operators, 2 are native; "add used groups", "clear unsed groups". 2 new are added is "delete all groups", "reset all groups" --- LightGroup.py | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/LightGroup.py b/LightGroup.py index 02904b3..164e19b 100644 --- a/LightGroup.py +++ b/LightGroup.py @@ -3,7 +3,7 @@ # to select the Environment so it can be reassigned via Assign/Unassign like lights. import bpy -from bpy.types import Operator, Panel +from bpy.types import Operator, Panel, Menu from bpy.props import StringProperty # ------------------------------------------------------------------------- @@ -60,6 +60,28 @@ def execute(self, context): context.scene.light_group_filter = "" return {'FINISHED'} + +class LG_MT_lightgroup_context_menu(Menu): + bl_label = "LighGroup Specials" + + def draw(self, _context): + layout = self.layout + + layout.operator("scene.view_layer_add_used_lightgroups", icon='ADD') + layout.operator("scene.view_layer_remove_unused_lightgroups", icon='REMOVE') + + layout.separator() + layout.operator( + "lg.reset_all_lightgroups", + icon='LOOP_BACK', + text="Reset All LighGroups", + ) + layout.operator( + "lg.remove_all_lightgroups", + icon='NONE', + text="Delete All LighGroups", + ) + # ------------------------------------------------------------------------- # Operators # ------------------------------------------------------------------------- @@ -226,6 +248,67 @@ def execute(self, context): self.report({'WARNING'}, "Lightgroups not available in this Blender version.") return {'FINISHED'} + + +class LG_ResetAllLighgrGroups(Operator): + """Remove all lights from any LightGroups.""" + bl_idname = "lg.reset_all_lightgroups" + bl_label = "Reset All LightGroups" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + view_layer = context.view_layer + # Unassign lights from the group before removing the group + for obj in context.scene.objects : + print(obj.type) + if obj.type == 'LIGHT' and (obj.lightgroup != ""): + obj.lightgroup = "" + + # Force redraw panel so lightgroup gets updated + for area in context.screen.areas: + if area.type == 'PROPERTIES': + area.tag_redraw() + + for obj in bpy.data.worlds: + if obj.lightgroup != "": + obj.lightgroup = "" + + # Force redraw panel so lightgroup gets updated + for area in context.screen.areas: + if area.type == 'PROPERTIES': + area.tag_redraw() + + else: + self.report({'INFO'}, "All Lightgroups cleared.") + return {'FINISHED'} + + +class LG_RemoveAllLighgrGroups(Operator): + """Remove all LightGroups.""" + bl_idname = "lg.remove_all_lightgroups" + bl_label = "Remove All the LightGroups in the scene. Also removes them from the lights." + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + view_layer = context.view_layer + # Unassign lights from the group before removing the group + if hasattr(view_layer, "lightgroups"): + for index, item in reversed(list(enumerate(view_layer.lightgroups))): + view_layer.active_lightgroup_index = index + + # Use the Blender operator to remove the lightgroup + bpy.ops.scene.view_layer_remove_lightgroup() + + # Force redraw panel so lightgroup gets updated + for area in context.screen.areas: + if area.type == 'PROPERTIES': + area.tag_redraw() + + else: + self.report({'INFO'}, "All Lightgroups Removed.") + return {'FINISHED'} + + # ------------------------------------------------------------------------- # Drawing # ------------------------------------------------------------------------- @@ -274,6 +357,8 @@ def draw(self, context): col = row.column(align=True) col.operator("lg_editor.add_light_group", icon='ADD', text="") col.operator("lg_editor.remove_light_group", icon='REMOVE', text="") + + col.menu("LG_MT_lightgroup_context_menu", icon='DOWNARROW_HLT', text="") else: col.label(text="No Lightgroups in this Blender version", icon='ERROR') @@ -371,6 +456,9 @@ def draw(self, context): LG_AddLightGroup, LG_RemoveLightGroup, LG_ClearFilter, + LG_MT_lightgroup_context_menu, + LG_ResetAllLighgrGroups, + LG_RemoveAllLighgrGroups, ) def register():