From 2a95535e0a4f131ace43f179da8459007691967a Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Wed, 12 Feb 2025 21:06:59 -0700 Subject: [PATCH] Commit before rebuilding in OdenGraphQT --- Experimental/experimental_nodes.py | 102 ++ .../Orphaned Code/data_collector.py | 0 Nodes/__pycache__/__init__.cpython-312.pyc | Bin 134 -> 134 bytes Nodes/__pycache__/array_node.cpython-312.pyc | Bin 2807 -> 2808 bytes .../__pycache__/average_node.cpython-312.pyc | Bin 2070 -> 0 bytes .../__pycache__/backdrop_node.cpython-312.pyc | Bin 0 -> 9706 bytes .../base_circle_node.cpython-312.pyc | Bin 0 -> 1690 bytes Nodes/__pycache__/base_node.cpython-312.pyc | Bin 0 -> 36498 bytes .../character_status_node.cpython-312.pyc | Bin 8369 -> 8370 bytes .../convert_to_percent_node.cpython-312.pyc | Bin 3189 -> 0 bytes Nodes/__pycache__/data_node.cpython-312.pyc | Bin 6173 -> 6062 bytes .../__pycache__/display_node.cpython-312.pyc | Bin 4260 -> 0 bytes .../display_string_node.cpython-312.pyc | Bin 5262 -> 0 bytes .../display_text_node.cpython-312.pyc | Bin 5843 -> 0 bytes .../display_value_node.cpython-312.pyc | Bin 5520 -> 0 bytes Nodes/__pycache__/group_node.cpython-312.pyc | Bin 0 -> 7825 bytes .../math_operation_node.cpython-312.pyc | Bin 0 -> 4850 bytes .../__pycache__/multiply_node.cpython-312.pyc | Bin 1899 -> 0 bytes .../number_input_node.cpython-312.pyc | Bin 3087 -> 0 bytes Nodes/__pycache__/port_node.cpython-312.pyc | Bin 0 -> 5089 bytes .../__pycache__/subtract_node.cpython-312.pyc | Bin 1872 -> 0 bytes Nodes/array_node.py | 4 +- Nodes/average_node.py | 36 - Nodes/backdrop_node.py | 184 ++++ Nodes/base_circle_node.py | 46 + Nodes/base_node.py | 876 ++++++++++++++++++ Nodes/character_status_node.py | 2 +- Nodes/convert_to_percent_node.py | 65 -- Nodes/data_node.py | 4 +- Nodes/group_node.py | 176 ++++ Nodes/math_operation_node.py | 121 +++ Nodes/multiply_node.py | 36 - Nodes/port_node.py | 135 +++ Nodes/subtract_node.py | 37 - debug_processed.png | Bin 2824 -> 399 bytes debug_screenshot.png | Bin 13623 -> 3435 bytes 36 files changed, 1645 insertions(+), 179 deletions(-) create mode 100644 Experimental/experimental_nodes.py rename data_collector.py => Legacy_Code/Orphaned Code/data_collector.py (100%) delete mode 100644 Nodes/__pycache__/average_node.cpython-312.pyc create mode 100644 Nodes/__pycache__/backdrop_node.cpython-312.pyc create mode 100644 Nodes/__pycache__/base_circle_node.cpython-312.pyc create mode 100644 Nodes/__pycache__/base_node.cpython-312.pyc delete mode 100644 Nodes/__pycache__/convert_to_percent_node.cpython-312.pyc delete mode 100644 Nodes/__pycache__/display_node.cpython-312.pyc delete mode 100644 Nodes/__pycache__/display_string_node.cpython-312.pyc delete mode 100644 Nodes/__pycache__/display_text_node.cpython-312.pyc delete mode 100644 Nodes/__pycache__/display_value_node.cpython-312.pyc create mode 100644 Nodes/__pycache__/group_node.cpython-312.pyc create mode 100644 Nodes/__pycache__/math_operation_node.cpython-312.pyc delete mode 100644 Nodes/__pycache__/multiply_node.cpython-312.pyc delete mode 100644 Nodes/__pycache__/number_input_node.cpython-312.pyc create mode 100644 Nodes/__pycache__/port_node.cpython-312.pyc delete mode 100644 Nodes/__pycache__/subtract_node.cpython-312.pyc delete mode 100644 Nodes/average_node.py create mode 100644 Nodes/backdrop_node.py create mode 100644 Nodes/base_circle_node.py create mode 100644 Nodes/base_node.py delete mode 100644 Nodes/convert_to_percent_node.py create mode 100644 Nodes/group_node.py create mode 100644 Nodes/math_operation_node.py delete mode 100644 Nodes/multiply_node.py create mode 100644 Nodes/port_node.py delete mode 100644 Nodes/subtract_node.py diff --git a/Experimental/experimental_nodes.py b/Experimental/experimental_nodes.py new file mode 100644 index 0000000..d88b197 --- /dev/null +++ b/Experimental/experimental_nodes.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +""" +Standalone NodeGraphQT Math Node Example + +This example defines a custom "Math Node" that: + - Uses two text inputs for numeric operands (via add_text_input) + - Provides a combo box for operator selection (via add_combo_menu) + - Offers a checkbox to enable/disable the operation (via add_checkbox) + - Computes a result and updates its title accordingly. +""" + +from NodeGraphQt import NodeGraph, BaseNode + +class MathNode(BaseNode): + """ + Math Node: + - Operands: Two text inputs (Operand 1 and Operand 2) + - Operator: Combo box to select 'Add', 'Subtract', 'Multiply', or 'Divide' + - Enable: Checkbox to enable/disable the math operation + - Output: Result of the math operation (if enabled) + """ + __identifier__ = 'example.math' + NODE_NAME = 'Math Node' + + def __init__(self): + super(MathNode, self).__init__() + + # Add two text inputs for operands. + self.add_text_input('operand1', 'Operand 1', text='10') + self.add_text_input('operand2', 'Operand 2', text='5') + + # Add a combo box for operator selection. + self.add_combo_menu('operator', 'Operator', items=['Add', 'Subtract', 'Multiply', 'Divide']) + + # Add a checkbox to enable/disable the operation. + self.add_checkbox('enable', 'Enable Operation', state=True) + + # Add an output port to transmit the result. + self.add_output('Result') + + self.value = 0 + self.set_name("Math Node") + self.process_input() + + def process_input(self): + """ + Gather values from the widgets, perform the math operation if enabled, + update the node title, and send the result to connected nodes. + """ + try: + op1 = float(self.get_property('operand1')) + except (ValueError, TypeError): + op1 = 0.0 + try: + op2 = float(self.get_property('operand2')) + except (ValueError, TypeError): + op2 = 0.0 + + operator = self.get_property('operator') + enable = self.get_property('enable') + + if enable: + if operator == 'Add': + result = op1 + op2 + elif operator == 'Subtract': + result = op1 - op2 + elif operator == 'Multiply': + result = op1 * op2 + elif operator == 'Divide': + result = op1 / op2 if op2 != 0 else 0.0 + else: + result = 0.0 + else: + result = 0.0 + + self.value = result + self.set_name(f"Result: {result}") + + output_port = self.output(0) + if output_port and output_port.connected_ports(): + for connected_port in output_port.connected_ports(): + connected_node = connected_port.node() + if hasattr(connected_node, 'receive_data'): + connected_node.receive_data(result, source_port_name='Result') + + +if __name__ == '__main__': + import sys + try: + from PySide2.QtWidgets import QApplication + except ImportError: + from PySide6.QtWidgets import QApplication + + app = QApplication(sys.argv) + graph = NodeGraph() + graph.register_node(MathNode) + node = graph.create_node('example.math.MathNode', name='Math Node') + node.set_pos(100, 100) + graph.widget.resize(1200, 800) + graph.widget.setWindowTitle("NodeGraphQT Math Node Demo") + graph.widget.show() + sys.exit(app.exec_()) diff --git a/data_collector.py b/Legacy_Code/Orphaned Code/data_collector.py similarity index 100% rename from data_collector.py rename to Legacy_Code/Orphaned Code/data_collector.py diff --git a/Nodes/__pycache__/__init__.cpython-312.pyc b/Nodes/__pycache__/__init__.cpython-312.pyc index d824339f20935dab387406637be18ab08f265886..a4bdc8d8c13cd6b1d671acecbeae5bed3714101f 100644 GIT binary patch delta 24 ecmZo;Y-8j;&CAQh00hkzYbSD>FuF~2a038BH3gpl delta 24 ecmZo;Y-8j;&CAQh00b9g)=cC!VN9Fo;06Fh;|1>k diff --git a/Nodes/__pycache__/array_node.cpython-312.pyc b/Nodes/__pycache__/array_node.cpython-312.pyc index 5a2ef80d5b90c8b7f0b50c45c0fccccaac9081c2..62dd897dd4dee9890d54343ea356f50b76a59381 100644 GIT binary patch delta 103 zcmew^`a_iWG%qg~0}yNrTbq7(BkyrW3Gt-Tyu3=?oWvx(%zVAXqN2pg_`Lj-)XlFM z*D^A?O`geY&nU3@Ci6BHM(NEHIKD7)={NY@5D;(h{K7H0k1LAbfKhJ($5#d*RU{5H F82~#>A$b4* delta 102 zcmew%`dyUwG%qg~0}#y6T$8?UBkyrWak0#Nz4XkIjM5~%yv*eMoK(HUqN2pg&2Jdj zGBT!3p2cj>$hY|x^EMVn$;}fvzA$p>HTc~S5O47O!ZNv^D~ex_QEvjrR|X(eBn~tf E0RBNB+yDRo diff --git a/Nodes/__pycache__/average_node.cpython-312.pyc b/Nodes/__pycache__/average_node.cpython-312.pyc deleted file mode 100644 index 322739962186ad62c8bdeb7dd48bd6ce28edf451..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2070 zcmah}O>7%Q6rR~1du_*dL!87eDB6;=_0nLq0eY|yB5tb^P*eT{sui@_dd9BP&8|DU zZkx!Ff)oi?1d&RhL@G!{>H*Xqdh8K;LgLakiE0KoapIPef~uaFS?_LA5HQltzM1#U zdvD(R=KYXN#sM3jX*X*3BmjQpi(tsUu(gi~D*yomO)vw?5CQ=l2S`{1NW}2E*u|BF zSxJ+B;|mQ^rDQmG(nh$_)plpWUu+#@!bi*~24#pqSwOHXBB3lHu`IVi6(VU-DMygJ z2+GkmxCqP2J3x!j#Q1qkjGAY7ZZ@v6;qR#2SJB&1)#ud-tI>3wqB=gRmPXYhYTiK8 zO~*A(n^-j*)xsEIq=oR4O_$+|>f6|9ny#96&oEfmm1p*Xvf@)pZ&Wg<3ZCyFg8OXM!r9?;R`80JYwd|}bPyd&k zzLZ{7)|EY-{sDF@&Y{Um$z^jx8Qx;n!4vf1Pt>CiVZ~knH82kmJPJO*0L%-l1Usq5 z53|t<%RVo*#1D71#OaVqT?C&4vRRPXlvz2T+;sVa;BB0_KqEZw{SE{dnX1*plBnn)A| zA^c{9JM#!kc(8M^ki}XLmAe#8S8U5-M8imL*u-_H#HmFiMx%i(MCGb!8!nC3uxq$3 zq4D=P$&V3Y6B>JWw&9DS)10BAi7n3&d5U?{6vDGXc)&EqZF{t*5*KMKbW2mA=tV#i z4TE5dp?MslalYp83-JcZ2kGbbk_}>4FncrJCFXKFAHWl;0fu&Nv1tKxdi&aupK|&3 zM5k|XrFglxT3_qSw~uzl4&NCotxET^qj$5T>)FEhLVJRyvzf)q0#$8 zhwcs?S{r&{Ik72-y}3?i_sWUOC$8@AH5w@1>Y!}9HyAe*%q?>xMZu+9_L^%)yAO)mH7`njfI`kJV& zBfG-3Q>-l@meqQ-jtN`GCQlwcrcaKaIOeqwlJ~OeccKb5OmE1PjJaByL=vn#hS@laCakv}sjStC6C-T}r-n&AEEI58S$-f{`I r;Ay@AU6fZ><>l^0ULj*l&pkUl29^p4e*x-4X%Ezv^M3&L^G^K*w)NoH diff --git a/Nodes/__pycache__/backdrop_node.cpython-312.pyc b/Nodes/__pycache__/backdrop_node.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c34472237e4f4f528ba6921c6c82125632d56674 GIT binary patch literal 9706 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 Y@8~b7SH5b9yXf9qCx1oZFWJfe03`&^lmGw# literal 0 HcmV?d00001 diff --git a/Nodes/__pycache__/base_circle_node.cpython-312.pyc b/Nodes/__pycache__/base_circle_node.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..571e8f6ba1030ba728d92bda6616875756728d71 GIT binary patch literal 1690 zcma)6O^;hO6t(l!%#;jbRUr^Ebdf}AUPmK!6Ae{c7|JXr1A+yLMs+;TPU7;u#`bHI zszS4C~jLFFeVAqL0eOnto+k zZP$LsPMc=+Yr?8tEEU8XZ)xKPd2iRL&@b>sJzGppTG0O#wp64~CFz$W?U!Y#Uy+sj zWJqDNwSM(Jnb3ai1JbT$8*~1GTSP##&Y%o-!z56liky;6M*}t(+}iXmd0T_QdJgz- ztY8eKT{j{bzwf+Ns=I!#gUw=n~!v#$u?kW;;Y25 z+uDtpU{Vi<%788=mDQujE97_{s2nS!orrwZky?z52)kYOe1)eALoreS^}LH*V{9)* zkQeuJzr7?HDRtH|~eH{#E;G}1?@VwfA#5-ik1 ztqhZKC^Yzppn2i^6u>!qlQnhh-So#Ix;315BRGomuz9Klmz{?L&`yg04rVCQz!e)s zaN?`nz5Ta$c<<`o4$|&T^PK<11q=)%03YWqtAZgUGnY1RZJH?4TFU(PYxl1S1)+lkcIph&7B1)#@A?>v*Sv5DzAR?>J zJuOpRn6WE$2;aFQ-fdRopY-fb8$c&n6&4hdU{NLt~Wl5zLG!GFMNID$Ijk&pZrws zoxl_^b*S2BP%>U6&a-vS!&s&P#xHPwl!{`eDo*5HedP=`QpdS literal 0 HcmV?d00001 diff --git a/Nodes/__pycache__/base_node.cpython-312.pyc b/Nodes/__pycache__/base_node.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..739b435f717fac9741a914791c363fb7e9e60fd5 GIT binary patch literal 36498 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

Fy;-8(OK&W0Dt zTV~v8cZue_eaqeWi%)>JtR{eI-agGH+Wx|1%o+PoYG(2owvYb*~o9xYDJSOQnse(#9W@HqY!!cWhef z*puqmv)J+2!rI4E9goeOn6+QYznuS#g87ceQfnWd>AukT#V6s0Ro3#ZuVTjg^E(cq zYM*VHM9Uj%_m$>lh(f>0`0|DO!xs~6P24NO@I3c~F(jVJ|CIn+y8LelW#PmcW6afZ z8467{=;DWUg8#4*Ry3tbn|@H*a(`H{-i#G;3e_AWjMpyO&&I?V`mE225~AhzmnM;w zO>xI5h_Hr-@_lTQ7A#L5FVre${6eoTGVZjHMsV_nbFKlEJ;hmLu)jTT;Ep~1oM82g?hyn?YAlPyV#iLvk?eVb4{Ih%`A zde)R7y#aRia&8tzz|}1DVMqQwN8|gBy&&)h=*yg$uQQoL@4B2#*)>sD zpQ3!E>FZ0>BzpPXSF2ffvlGm#j=kt95&>UAw4|T z)g!6Otuvl9zGXF;Dr{t4c6*oVx>I%CZ%GSvPc4)kp7$Q6!>^fp-S(7wdm6bT7bCAe zM}d)xBdMuIKrUL~(1bN2i9;fUNm^{;XW?fdq z8FD9?z_PAYNcCMktnrx$m35H>3TvPvRE=)YkzN-gaX2#jJXA=YpcOm=!Hj8_$k^*n zXl2-bjBod{0}gViE7!wSk5`juk*jYLdakZRXEz{ou}>3b^@#~tRyFEUwQ=`3lZp4) zX~U}fK?9LAYa+mC!0Qe+vH29I$&Sy+F;i#OyxoRJ%)dsdyUWB)7`xZ7EJN${OzD?* zA*2-Z2?;{V`Z{aMmg`UTS)Q1zyBiVyIZfD8C>axWjZj*7$$QZYm)7aa)2WgUNYR+E zavEma6@!YX4I8!Bq7@ZIYTNEt|$z zSEzZywk$YnJTt{uP$;RN*N!Mwu@SWp@Qv(;g4EpwDN=KFRNu!wfFg(KOS{Ib{Qn_G z(TOGxWvO4HEOiV-Fu+IpV<|_eNN6cX$+s}Av*aKP!H5^32}ar-Q98w|-rbL2q-HVr zR+eBS{}8pxBB4y`(cZj*)RX<#2LdMlEwzw5n=0?w6ea zm)I`nkju>04q((y;6uUijExKph4H~G@$@ih_+l&!*Xv9zoxIsbLm3PC?R{9^Q9m)v zB~+x*9q6>LowAiToVpv)zTQecxcE#gc9}gtj<0m-Ts9gXiVPA{mB(YoVO`-<_()Np z-LL`VenHMlJN$l7gS@(1)E0+#CyUF_o}k zkDRfdBBNF^Tn1T{?k9BFC>ZV;V%dK3L$$p4cHyC5| zT4%}sT4}wnrEZookCZ;P%pM;SdwP#O)5pvSrm^}pz+Qd4ex*?NG%Stg(KIYhz4CiN zO!<9)OcbruvCy2+w4!18{J05GMGaBeH{txYKEI8fqvE&s_Ci4n;g!7zxmd^mnNbIE zo5xm`5BBvQq%bYD^|7$+pnsF%4l+YyFsOKfw6AkK8b;U~3_d>|ijtuUclp7f6dy#O zp!aB3cd&Qgk#4z)n!SlYD}l!dbQ3s0ppC#&1jsW-9wHDUkRUKY-~|FN6JQR4-=NSH z0(&PN}H*#uV--usB4}1_={^4cer*_fhzTIWRN7i5J zdEe=9)!y-VTzlWIs&yT(z28~sYFG~ByV~xQ<+~d0lzCi@cdC4@jmu3S!?L5nReyWGO?c9F$c97H^!r1$UR%0)9lKxNZ!34zEw=;QZWo@gJ!MNjx@Xzp zcU9c}v`y%-9kHc5_bxlyTorfvY{jmvcf?}XwmV`SUc4J%d3T{}%bgkzK-C_^y=cpK zZKrzG5!gXs9o4Y;PE~=c>CPIDt3?l0-l_7q09st0JBMu*uKGJ2rl8N|`&m6x%Bo0Gse9B*?sT z+hMcW{-@CVcfz3`3LQTbHvEmS|L^TioBgK(!22G-w)5}pE}MN>0C=zD(?vGl%;ft5 J{W4be{{S2@;idop literal 0 HcmV?d00001 diff --git a/Nodes/__pycache__/character_status_node.cpython-312.pyc b/Nodes/__pycache__/character_status_node.cpython-312.pyc index 85016833a160889b80b1ad100ccec4b134ca67e7..4ed2a54d240aea06e72481b71f468d25240be5d0 100644 GIT binary patch delta 66 zcmdn!xXF?CG%qg~0}!-@txZ3gIP`|3nz|C%=_H0sy~&7h3=T delta 65 zcmdnwxY3dKG%qg~0}zyjtVutyk=KfYF>SL4M<0`fcxJv{dS*#RX_8)EW^#T`s$OwP UVo7Q7=J#CxL>ToZzmq=#0I7)=1ONa4 diff --git a/Nodes/__pycache__/convert_to_percent_node.cpython-312.pyc b/Nodes/__pycache__/convert_to_percent_node.cpython-312.pyc deleted file mode 100644 index b1721cb97d9f1cd90857a9ffae38cefc87f8a61d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3189 zcmbVOO>7&-6`m!RfK@M~tN$gD|xn#$EXJ=p9weF641XxAB1vP(NCOXzt{GLoiIj`fCD#i1#rS<$oRnvh)+)-^o-^4E?he2-{U z;*7za6*QTCJjm`#`U02}I|1wT4r}n_7zm$1++~zzAodQ4NjoJ+nysP?ME)k1cF~BZ z-I5D>PZix@(%fYfcN;_2`Bzjc*7USeL{Ssh6n&-mVIr@HYF18&ol7Ylc-dbOF3oQA zKI!ktKcM>qa~J9MlHTWQ@Gysz?Hp$u3kk~Tf zyEvt124B=hbhhv1*`5srGNR|EE{{+mUJ&0mq!bH?I zpy4jPq37{N)uHvUQej@9VPmD@01ayKg;uulR&l;Uqi2KlBE5$t8)X&ebF*w0Hx;(b zO3X)-2ob+-BfW>sPnoi_%$9%?5?cbR6<66Mx$dzQci9biqY*G0c=dl^7IxW)?OHOC z-rpN_CCT$Pi0fzq>1M8{^KT$BRPjjevgaQ9BgC%f&j*m_E1utaZe8g7uQ6-rhVuio z#>BZ4p)hLZCxI}^!F$#agT(Nmbv^C&?&G#I=Fm+lKE8LM&W%1CitG$cH-@IS`R34k)!Pn7cfyHA zI8mQ{rx`v|^*?64-ov|*gIh{7GFe^PjmB#JW^}sxt9B^5C43i(?S>E6Cr>rQbM^kY z_6V-cHAWI&O*F#CtLNJjQ`^6&&C~-2+oMy}#aqjdeaIW!OVroj{B!hA(JFh}zv;iT z^c{ET+0!8EAN>(A-ox$J#%o7w6I%zXi?^3Im;dwF36no-5%{RTyHB1?qOX(Qg#qW+ z^S*_E^P7MRWYI1QphPKUl6|^Ec82`E%uisJ5(ciOfRX~41Al>R@cJ8BeOuRQ25R#wRc;gnZHdv_|4Az#m4-_)}{Ab$9~-&8Q&R+H%8(+ zBQuSWnQi64U~^=?-ao%P7~PuN_Ix$i96VkR9B+pXkTCcH!G-q#jU&7Vd_131rDB%K z0iOT3C}gdbJ}APFmUJbf$b{!RIgVnCyhS@22qS1c1w=WU<@q>6hG15snfRSfxajM{ ze1gzwqK!fK>?Udt&u*mJ{cmh6J!J!~(I3Z#UDN+g_+5jhd!~8{9*iEImsAn_^3$ip zc_I{6F6o3K(Qtr3Yn7?{7u4Bj=0fi6&gYyar(vCXq8)+`vIE2Xz`7asr+&n|{yiFg Q$|jk?tzZ6x;LqIuUz#rl(*OVf diff --git a/Nodes/__pycache__/data_node.cpython-312.pyc b/Nodes/__pycache__/data_node.cpython-312.pyc index 03561c1154dd9cee58698fe7ee8a8092d90825ef..7536ed3e34c54bcaa59fe203ca7013ae65f16ebc 100644 GIT binary patch delta 400 zcmbPhuuh-%G%qg~0}xmxuT9U{$ji+vE|yf9mshEqlbEEJnXi|USdti@m!FclS&w-+ zBct2qYb;^ROePGIxK)LjYM5#m;u%5OL7W(*%$h8d(>d~Gi`ak$`e`!X;z>^} zi7zP1FGwvasVoB7QzSC^B}XWu`eb`f8NME_8yr0Syq&x=IInUjZ;s{EWMnko+|K3C z!f3zwIv)oUW5nbK0dr zVq};c$Sp3&yqXax0R*+oHOxg}HO!M2FpEsK7vNwiaz$_^`-*Tga!+2!Bc)KwTEYv_ z!N5?%n!>W03BoI8>`|y;&ElUtSwxkcwGs$5*(UE7iO~dEUIbEfi>D|xIW@B^H69d7 zewrLbx*$>J;*z2w5g==_m1r+p0gzGLI{B=qG!NXzSAaf#Au7r!rPJVdLqNL0^9u_n stI8)aE>=gz&k}4vrod!zaXCib$!6lI!kLVW9*i9oUm3uRA_<^@0HH8-W&i*H diff --git a/Nodes/__pycache__/display_node.cpython-312.pyc b/Nodes/__pycache__/display_node.cpython-312.pyc deleted file mode 100644 index ff296552b96f472cd7b38c55400db23a79db9417..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4260 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 Zdhc+xcX)$9XG8o3cj(sqX9OB%@&76f%aZ^A diff --git a/Nodes/__pycache__/display_string_node.cpython-312.pyc b/Nodes/__pycache__/display_string_node.cpython-312.pyc deleted file mode 100644 index 425735b0e5e2f6684b14a3dad3779a04b2adc075..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5262 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 LnSM(k;{*R2fJaR8 diff --git a/Nodes/__pycache__/display_text_node.cpython-312.pyc b/Nodes/__pycache__/display_text_node.cpython-312.pyc deleted file mode 100644 index edc29d3397cc7befa0cf50dc0a0f77fbe1d498bd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5843 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 diff --git a/Nodes/__pycache__/group_node.cpython-312.pyc b/Nodes/__pycache__/group_node.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9381b3b49152b12aac3e8e06fcdee01e1721d7fc GIT binary patch literal 7825 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�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 diff --git a/Nodes/__pycache__/number_input_node.cpython-312.pyc b/Nodes/__pycache__/number_input_node.cpython-312.pyc deleted file mode 100644 index 1e03916f74ead978913aa2bf5cbc52892bb7537e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3087 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 Zb5iP^*EbpbY+i_b1s=Zjl)(>~{4dNc!u-|zXA zb3W&sU&7%K;Q90U+w<2XfIs-fJ93NJeF2#b&_E*wERrlCAix>WgjLW)o!ko|oSYMe%q~QT~>4pQ6!;=uELxwyjijDiKZlkF#YP zUsKU{b*pUH(byC!rsJ-7d9Yw6=L`15@`YrvkT(rIX|;ke#fy6CI+rl@M6Sg=)<1#_B)JeG=uIBU9PWAde(QP$%ElPuksV?kBT z8#&8TRf~Izt~?%}OPzn4nhSc~R?{Zcb4I~B&tb97d$m-Hcukg;Xa|}s_j-lpN=Yw% z1Vz@XNcLCYp%Pv<>eHLb__mU0C>_@#Ymu6@tqeWOcdm8Tm2G9D(c4#*u7-Be9G-$p z!3P(v43>HW3Q!>$nT8KO1gHoI%ahP@V?2&u^or<+pLIFn0{8ID@xU#Td8kN^M2|a? zCJdrhMRw!`ABF=n&!Q3tg6+41?StwBG@J%+usm2D;s~eE$$A#y_4tn@aEK=JX!s5A z?=p^brOj!>7Vv#ze%%N0Jx)Wdui$GxOM$%5cwRe-?$7{ z#B*?&#Fb23puE78=ioLM?L=~*o{GkUOm4PWXWlFpv2AosEt%A|n8f>p z$#aI8vsv4`Zs%;9ve5gy15Z0<)~gnZ5};9-8EtH|IRlJKlBiH9WGHbuVd@GA(oGLDYEB3QPt^-sJ+bO^x7{yT_}6M(Y}NY4bFF{>L=x#(UP>Os%GZ#If9d}VVMha1)cJJlbM}9t!Ad)I_<6n zI_B26wO;bNVW?`HP~N~6pVHS|5ZtBZ5N8aRJ)Szp{eDftek2l@)}K|oJY3$kYFhYc@zLL QvL{8!MD68AfFIZHKV(n6F8}}l diff --git a/Nodes/array_node.py b/Nodes/array_node.py index b069fe2..9403b51 100644 --- a/Nodes/array_node.py +++ b/Nodes/array_node.py @@ -8,9 +8,11 @@ class ArrayNode(BaseNode): - Stores incoming values in an array with size defined by ArraySize. When full, it removes the oldest value. """ - __identifier__ = 'io.github.nicole.array' + __identifier__ = 'bunny-lab.io.array_node' NODE_NAME = 'Array' + + def __init__(self): super(ArrayNode, self).__init__() self.values = {} # Ensure values is a dictionary. diff --git a/Nodes/average_node.py b/Nodes/average_node.py deleted file mode 100644 index 2f8be35..0000000 --- a/Nodes/average_node.py +++ /dev/null @@ -1,36 +0,0 @@ -from NodeGraphQt import BaseNode - -class AverageNode(BaseNode): - """ - Average Node: - - Inputs: A, B, C (adjustable as needed) - - Output: Result (the average of the inputs) - """ - __identifier__ = 'io.github.nicole.average' - NODE_NAME = 'Average' - - def __init__(self): - super(AverageNode, self).__init__() - self.values = {} # Ensure values is a dictionary. - self.add_input('A') - self.add_input('B') - self.add_input('C') - self.add_output('Result') - self.value = 0 - self.set_name("Average: 0") - - def process_input(self): - values = [] - for port_name in ['A', 'B', 'C']: - port = self.input(port_name) - connected = port.connected_ports() if port is not None else [] - if connected: - connected_port = connected[0] - parent_node = connected_port.node() - try: - values.append(float(getattr(parent_node, 'value', 0))) - except (ValueError, TypeError): - pass - avg = sum(values) / len(values) if values else 0 - self.value = avg - self.set_name(f"Average: {avg}") diff --git a/Nodes/backdrop_node.py b/Nodes/backdrop_node.py new file mode 100644 index 0000000..88968cb --- /dev/null +++ b/Nodes/backdrop_node.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 +""" +Enhanced Backdrop Node (Inherited from BaseNode) + +Features: + - Inherits from `BaseNode` so it can be discovered in your node scanning. + - Custom context menu to rename (set title) or pick a new color. + - Forces geometry updates to reduce "ghosting" or partial redraws. +""" + +from Qt import QtWidgets, QtGui +from NodeGraphQt import BaseNode +from NodeGraphQt.constants import NodePropWidgetEnum +from NodeGraphQt.qgraphics.node_backdrop import BackdropNodeItem + + +class BackdropNode(BaseNode): + """ + Backdrop Node: + - Allows grouping or annotating other nodes by resizing a large rectangle. + - Provides a custom context menu for renaming and recoloring (via on_context_menu). + """ + + __identifier__ = 'bunny-lab.io.backdrop' + NODE_NAME = 'Backdrop' + + def __init__(self): + # Use BackdropNodeItem for the specialized QGraphicsItem. + super(BackdropNode, self).__init__(qgraphics_item=BackdropNodeItem) + + # Default color (teal). + self.model.color = (5, 129, 138, 255) + + # Multi-line text property for storing the backdrop text. + self.create_property( + 'backdrop_text', + '', + widget_type=NodePropWidgetEnum.QTEXT_EDIT.value, + tab='Backdrop' + ) + + # -------------------------------------------------------------------------- + # Resizing / Geometry + # -------------------------------------------------------------------------- + def on_backdrop_updated(self, update_prop, value=None): + """ + Triggered when the user resizes or double-clicks the backdrop sizer handle. + """ + if not self.graph: + return + + if update_prop == 'sizer_mouse_release': + # User finished dragging the resize handle + self.view.prepareGeometryChange() + self.graph.begin_undo(f'resized "{self.name()}"') + self.set_property('width', value['width']) + self.set_property('height', value['height']) + self.set_pos(*value['pos']) + self.graph.end_undo() + self.view.update() + + elif update_prop == 'sizer_double_clicked': + # User double-clicked the resize handle (auto-resize) + self.view.prepareGeometryChange() + self.graph.begin_undo(f'"{self.name()}" auto resize') + self.set_property('width', value['width']) + self.set_property('height', value['height']) + self.set_pos(*value['pos']) + self.graph.end_undo() + self.view.update() + + def auto_size(self): + """ + Auto-resize the backdrop to fit around intersecting nodes. + """ + if not self.graph: + return + self.view.prepareGeometryChange() + self.graph.begin_undo(f'"{self.name()}" auto resize') + size = self.view.calc_backdrop_size() + self.set_property('width', size['width']) + self.set_property('height', size['height']) + self.set_pos(*size['pos']) + self.graph.end_undo() + self.view.update() + + def wrap_nodes(self, nodes): + """ + Fit the backdrop around the specified nodes. + """ + if not self.graph or not nodes: + return + self.view.prepareGeometryChange() + self.graph.begin_undo(f'"{self.name()}" wrap nodes') + size = self.view.calc_backdrop_size([n.view for n in nodes]) + self.set_property('width', size['width']) + self.set_property('height', size['height']) + self.set_pos(*size['pos']) + self.graph.end_undo() + self.view.update() + + def nodes(self): + """ + Return a list of nodes wrapped by this backdrop. + """ + node_ids = [n.id for n in self.view.get_nodes()] + return [self.graph.get_node_by_id(nid) for nid in node_ids] + + def set_text(self, text=''): + """ + Set the multi-line text in the backdrop. + """ + self.set_property('backdrop_text', text) + + def text(self): + """ + Return the text content in the backdrop. + """ + return self.get_property('backdrop_text') + + def set_size(self, width, height): + """ + Manually set the backdrop size. + """ + if self.graph: + self.view.prepareGeometryChange() + self.graph.begin_undo('backdrop size') + self.set_property('width', width) + self.set_property('height', height) + self.graph.end_undo() + self.view.update() + else: + self.view.width, self.view.height = width, height + self.model.width, self.model.height = width, height + + def size(self): + """ + Return (width, height) of the backdrop. + """ + self.model.width = self.view.width + self.model.height = self.view.height + return self.model.width, self.model.height + + # No ports for a backdrop: + def inputs(self): + return + + def outputs(self): + return + + # -------------------------------------------------------------------------- + # Custom Context Menu + # -------------------------------------------------------------------------- + def on_context_menu(self, menu): + """ + Called manually by the node context menu callback in older NodeGraphQt versions. + """ + rename_action = menu.addAction("Set Title...") + rename_action.triggered.connect(self._change_title) + + color_action = menu.addAction("Set Color...") + color_action.triggered.connect(self._change_color) + + def _change_title(self): + """ + Prompt for a new backdrop title (header). + """ + new_title, ok = QtWidgets.QInputDialog.getText( + None, "Backdrop Title", "Enter new backdrop title:" + ) + if ok and new_title: + self.set_name(new_title) + + def _change_color(self): + """ + Prompt for a new backdrop color via QColorDialog. + """ + current_color = QtGui.QColor(*self.model.color) + color = QtWidgets.QColorDialog.getColor( + current_color, None, "Select Backdrop Color" + ) + if color.isValid(): + self.model.color = (color.red(), color.green(), color.blue(), color.alpha()) + self.view.update() diff --git a/Nodes/base_circle_node.py b/Nodes/base_circle_node.py new file mode 100644 index 0000000..790c8f6 --- /dev/null +++ b/Nodes/base_circle_node.py @@ -0,0 +1,46 @@ +#!/usr/bin/python +from NodeGraphQt.nodes.base_node import BaseNode +from NodeGraphQt.qgraphics.node_circle import CircleNodeItem + + +class BaseNodeCircle(BaseNode): + """ + `Implemented in` ``v0.5.2`` + + The ``NodeGraphQt.BaseNodeCircle`` is pretty much the same class as the + :class:`NodeGraphQt.BaseNode` except with a different design. + + .. inheritance-diagram:: NodeGraphQt.BaseNodeCircle + + .. image:: ../_images/node_circle.png + :width: 250px + + example snippet: + + .. code-block:: python + :linenos: + + from NodeGraphQt import BaseNodeCircle + + class ExampleNode(BaseNodeCircle): + + # unique node identifier domain. + __identifier__ = 'io.jchanvfx.github' + + # initial default node name. + NODE_NAME = 'My Node' + + def __init__(self): + super(ExampleNode, self).__init__() + + # create an input port. + self.add_input('in') + + # create an output port. + self.add_output('out') + """ + + NODE_NAME = 'Circle Node' + + def __init__(self, qgraphics_item=None): + super(BaseNodeCircle, self).__init__(qgraphics_item or CircleNodeItem) \ No newline at end of file diff --git a/Nodes/base_node.py b/Nodes/base_node.py new file mode 100644 index 0000000..4212bb5 --- /dev/null +++ b/Nodes/base_node.py @@ -0,0 +1,876 @@ +#!/usr/bin/python +from collections import OrderedDict + +from NodeGraphQt.base.commands import NodeVisibleCmd, NodeWidgetVisibleCmd +from NodeGraphQt.base.node import NodeObject +from NodeGraphQt.base.port import Port +from NodeGraphQt.constants import NodePropWidgetEnum, PortTypeEnum +from NodeGraphQt.errors import ( + PortError, + PortRegistrationError, + NodeWidgetError +) +from NodeGraphQt.qgraphics.node_base import NodeItem +from NodeGraphQt.widgets.node_widgets import ( + NodeBaseWidget, + NodeCheckBox, + NodeComboBox, + NodeLineEdit +) + + +class BaseNode(NodeObject): + """ + The ``NodeGraphQt.BaseNode`` class is the base class for nodes that allows + port connections from one node to another. + + .. inheritance-diagram:: NodeGraphQt.BaseNode + + .. image:: ../_images/node.png + :width: 250px + + example snippet: + + .. code-block:: python + :linenos: + + from NodeGraphQt import BaseNode + + class ExampleNode(BaseNode): + + # unique node identifier domain. + __identifier__ = 'io.jchanvfx.github' + + # initial default node name. + NODE_NAME = 'My Node' + + def __init__(self): + super(ExampleNode, self).__init__() + + # create an input port. + self.add_input('in') + + # create an output port. + self.add_output('out') + """ + + NODE_NAME = 'Node' + + def __init__(self, qgraphics_item=None): + super(BaseNode, self).__init__(qgraphics_item or NodeItem) + self._inputs = [] + self._outputs = [] + + def update_model(self): + """ + Update the node model from view. + """ + for name, val in self.view.properties.items(): + if name in ['inputs', 'outputs']: + continue + self.model.set_property(name, val) + + for name, widget in self.view.widgets.items(): + self.model.set_property(name, widget.get_value()) + + def set_property(self, name, value, push_undo=True): + """ + Set the value on the node custom property. + + Args: + name (str): name of the property. + value (object): property data (python built in types). + push_undo (bool): register the command to the undo stack. (default: True) + """ + # prevent signals from causing a infinite loop. + if self.get_property(name) == value: + return + + if name == 'visible': + if self.graph: + undo_cmd = NodeVisibleCmd(self, value) + if push_undo: + self.graph.undo_stack().push(undo_cmd) + else: + undo_cmd.redo() + return + elif name == 'disabled': + # redraw the connected pipes in the scene. + ports = self.view.inputs + self.view.outputs + for port in ports: + for pipe in port.connected_pipes: + pipe.update() + super(BaseNode, self).set_property(name, value, push_undo) + + def set_layout_direction(self, value=0): + """ + Sets the node layout direction to either horizontal or vertical on + the current node only. + + `Implemented in` ``v0.3.0`` + + See Also: + :meth:`NodeGraph.set_layout_direction`, + :meth:`NodeObject.layout_direction` + + + Warnings: + This function does not register to the undo stack. + + Args: + value (int): layout direction mode. + """ + # base logic to update the model and view attributes only. + super(BaseNode, self).set_layout_direction(value) + # redraw the node. + self._view.draw_node() + + def set_icon(self, icon=None): + """ + Set the node icon. + + Args: + icon (str): path to the icon image. + """ + self.set_property('icon', icon) + + def icon(self): + """ + Node icon path. + + Returns: + str: icon image file path. + """ + return self.model.icon + + def widgets(self): + """ + Returns all embedded widgets from this node. + + See Also: + :meth:`BaseNode.get_widget` + + Returns: + dict: embedded node widgets. {``property_name``: ``node_widget``} + """ + return self.view.widgets + + def get_widget(self, name): + """ + Returns the embedded widget associated with the property name. + + See Also: + :meth:`BaseNode.add_combo_menu`, + :meth:`BaseNode.add_text_input`, + :meth:`BaseNode.add_checkbox`, + + Args: + name (str): node property name. + + Returns: + NodeBaseWidget: embedded node widget. + """ + return self.view.widgets.get(name) + + def add_custom_widget(self, widget, widget_type=None, tab=None): + """ + Add a custom node widget into the node. + + see example :ref:`Embedding Custom Widgets`. + + Note: + The ``value_changed`` signal from the added node widget is wired + up to the :meth:`NodeObject.set_property` function. + + Args: + widget (NodeBaseWidget): node widget class object. + widget_type: widget flag to display in the + :class:`NodeGraphQt.PropertiesBinWidget` + (default: :attr:`NodeGraphQt.constants.NodePropWidgetEnum.HIDDEN`). + tab (str): name of the widget tab to display in. + """ + if not isinstance(widget, NodeBaseWidget): + raise NodeWidgetError( + '\'widget\' must be an instance of a NodeBaseWidget') + + widget_type = widget_type or NodePropWidgetEnum.HIDDEN.value + self.create_property(widget.get_name(), + widget.get_value(), + widget_type=widget_type, + tab=tab) + widget.value_changed.connect(lambda k, v: self.set_property(k, v)) + widget._node = self + self.view.add_widget(widget) + #: redraw node to address calls outside the "__init__" func. + self.view.draw_node() + + #: HACK: calling the .parent() function here on the widget as it seems + # to address a seg fault issue when exiting the application. + widget.parent() + + def add_combo_menu(self, name, label='', items=None, tooltip=None, + tab=None): + """ + Creates a custom property with the :meth:`NodeObject.create_property` + function and embeds a :class:`PySide2.QtWidgets.QComboBox` widget + into the node. + + Note: + The ``value_changed`` signal from the added node widget is wired + up to the :meth:`NodeObject.set_property` function. + + Args: + name (str): name for the custom property. + label (str): label to be displayed. + items (list[str]): items to be added into the menu. + tooltip (str): widget tooltip. + tab (str): name of the widget tab to display in. + """ + self.create_property( + name, + value=items[0] if items else None, + items=items or [], + widget_type=NodePropWidgetEnum.QCOMBO_BOX.value, + widget_tooltip=tooltip, + tab=tab + ) + widget = NodeComboBox(self.view, name, label, items) + widget.setToolTip(tooltip or '') + widget.value_changed.connect(lambda k, v: self.set_property(k, v)) + self.view.add_widget(widget) + #: redraw node to address calls outside the "__init__" func. + self.view.draw_node() + + def add_text_input(self, name, label='', text='', placeholder_text='', + tooltip=None, tab=None): + """ + Creates a custom property with the :meth:`NodeObject.create_property` + function and embeds a :class:`PySide2.QtWidgets.QLineEdit` widget + into the node. + + Note: + The ``value_changed`` signal from the added node widget is wired + up to the :meth:`NodeObject.set_property` function. + + Args: + name (str): name for the custom property. + label (str): label to be displayed. + text (str): pre-filled text. + placeholder_text (str): placeholder text. + tooltip (str): widget tooltip. + tab (str): name of the widget tab to display in. + """ + self.create_property( + name, + value=text, + widget_type=NodePropWidgetEnum.QLINE_EDIT.value, + widget_tooltip=tooltip, + tab=tab + ) + widget = NodeLineEdit(self.view, name, label, text, placeholder_text) + widget.setToolTip(tooltip or '') + widget.value_changed.connect(lambda k, v: self.set_property(k, v)) + self.view.add_widget(widget) + #: redraw node to address calls outside the "__init__" func. + self.view.draw_node() + + def add_checkbox(self, name, label='', text='', state=False, tooltip=None, + tab=None): + """ + Creates a custom property with the :meth:`NodeObject.create_property` + function and embeds a :class:`PySide2.QtWidgets.QCheckBox` widget + into the node. + + Note: + The ``value_changed`` signal from the added node widget is wired + up to the :meth:`NodeObject.set_property` function. + + Args: + name (str): name for the custom property. + label (str): label to be displayed. + text (str): checkbox text. + state (bool): pre-check. + tooltip (str): widget tooltip. + tab (str): name of the widget tab to display in. + """ + self.create_property( + name, + value=state, + widget_type=NodePropWidgetEnum.QCHECK_BOX.value, + widget_tooltip=tooltip, + tab=tab + ) + widget = NodeCheckBox(self.view, name, label, text, state) + widget.setToolTip(tooltip or '') + widget.value_changed.connect(lambda k, v: self.set_property(k, v)) + self.view.add_widget(widget) + #: redraw node to address calls outside the "__init__" func. + self.view.draw_node() + + def hide_widget(self, name, push_undo=True): + """ + Hide an embedded node widget. + + Args: + name (str): node property name for the widget. + push_undo (bool): register the command to the undo stack. (default: True) + + See Also: + :meth:`BaseNode.add_custom_widget`, + :meth:`BaseNode.show_widget`, + :meth:`BaseNode.get_widget` + """ + if not self.view.has_widget(name): + return + undo_cmd = NodeWidgetVisibleCmd(self, name, visible=False) + if push_undo: + self.graph.undo_stack().push(undo_cmd) + else: + undo_cmd.redo() + + def show_widget(self, name, push_undo=True): + """ + Show an embedded node widget. + + Args: + name (str): node property name for the widget. + push_undo (bool): register the command to the undo stack. (default: True) + + See Also: + :meth:`BaseNode.add_custom_widget`, + :meth:`BaseNode.hide_widget`, + :meth:`BaseNode.get_widget` + """ + if not self.view.has_widget(name): + return + undo_cmd = NodeWidgetVisibleCmd(self, name, visible=True) + if push_undo: + self.graph.undo_stack().push(undo_cmd) + else: + undo_cmd.redo() + + def add_input(self, name='input', multi_input=False, display_name=True, + color=None, locked=False, painter_func=None): + """ + Add input :class:`Port` to node. + + Warnings: + Undo is NOT supported for this function. + + Args: + name (str): name for the input port. + multi_input (bool): allow port to have more than one connection. + display_name (bool): display the port name on the node. + color (tuple): initial port color (r, g, b) ``0-255``. + locked (bool): locked state see :meth:`Port.set_locked` + painter_func (function or None): custom function to override the drawing + of the port shape see example: :ref:`Creating Custom Shapes` + + Returns: + NodeGraphQt.Port: the created port object. + """ + if name in self.inputs().keys(): + raise PortRegistrationError( + 'port name "{}" already registered.'.format(name)) + + port_args = [name, multi_input, display_name, locked] + if painter_func and callable(painter_func): + port_args.append(painter_func) + view = self.view.add_input(*port_args) + + if color: + view.color = color + view.border_color = [min([255, max([0, i + 80])]) for i in color] + + port = Port(self, view) + port.model.type_ = PortTypeEnum.IN.value + port.model.name = name + port.model.display_name = display_name + port.model.multi_connection = multi_input + port.model.locked = locked + self._inputs.append(port) + self.model.inputs[port.name()] = port.model + return port + + def add_output(self, name='output', multi_output=True, display_name=True, + color=None, locked=False, painter_func=None): + """ + Add output :class:`Port` to node. + + Warnings: + Undo is NOT supported for this function. + + Args: + name (str): name for the output port. + multi_output (bool): allow port to have more than one connection. + display_name (bool): display the port name on the node. + color (tuple): initial port color (r, g, b) ``0-255``. + locked (bool): locked state see :meth:`Port.set_locked` + painter_func (function or None): custom function to override the drawing + of the port shape see example: :ref:`Creating Custom Shapes` + + Returns: + NodeGraphQt.Port: the created port object. + """ + if name in self.outputs().keys(): + raise PortRegistrationError( + 'port name "{}" already registered.'.format(name)) + + port_args = [name, multi_output, display_name, locked] + if painter_func and callable(painter_func): + port_args.append(painter_func) + view = self.view.add_output(*port_args) + + if color: + view.color = color + view.border_color = [min([255, max([0, i + 80])]) for i in color] + port = Port(self, view) + port.model.type_ = PortTypeEnum.OUT.value + port.model.name = name + port.model.display_name = display_name + port.model.multi_connection = multi_output + port.model.locked = locked + self._outputs.append(port) + self.model.outputs[port.name()] = port.model + return port + + def get_input(self, port): + """ + Get input port by the name or index. + + Args: + port (str or int): port name or index. + + Returns: + NodeGraphQt.Port: node port. + """ + if type(port) is int: + if port < len(self._inputs): + return self._inputs[port] + elif type(port) is str: + return self.inputs().get(port, None) + + def get_output(self, port): + """ + Get output port by the name or index. + + Args: + port (str or int): port name or index. + + Returns: + NodeGraphQt.Port: node port. + """ + if type(port) is int: + if port < len(self._outputs): + return self._outputs[port] + elif type(port) is str: + return self.outputs().get(port, None) + + def delete_input(self, port): + """ + Delete input port. + + Warnings: + Undo is NOT supported for this function. + + You can only delete ports if :meth:`BaseNode.port_deletion_allowed` + returns ``True`` otherwise a port error is raised see also + :meth:`BaseNode.set_port_deletion_allowed`. + + Args: + port (str or int): port name or index. + """ + if type(port) in [int, str]: + port = self.get_input(port) + if port is None: + return + if not self.port_deletion_allowed(): + raise PortError( + 'Port "{}" can\'t be deleted on this node because ' + '"ports_removable" is not enabled.'.format(port.name())) + if port.locked(): + raise PortError('Error: Can\'t delete a port that is locked!') + self._inputs.remove(port) + self._model.inputs.pop(port.name()) + self._view.delete_input(port.view) + port.model.node = None + self._view.draw_node() + + def delete_output(self, port): + """ + Delete output port. + + Warnings: + Undo is NOT supported for this function. + + You can only delete ports if :meth:`BaseNode.port_deletion_allowed` + returns ``True`` otherwise a port error is raised see also + :meth:`BaseNode.set_port_deletion_allowed`. + + Args: + port (str or int): port name or index. + """ + if type(port) in [int, str]: + port = self.get_output(port) + if port is None: + return + if not self.port_deletion_allowed(): + raise PortError( + 'Port "{}" can\'t be deleted on this node because ' + '"ports_removable" is not enabled.'.format(port.name())) + if port.locked(): + raise PortError('Error: Can\'t delete a port that is locked!') + self._outputs.remove(port) + self._model.outputs.pop(port.name()) + self._view.delete_output(port.view) + port.model.node = None + self._view.draw_node() + + def set_port_deletion_allowed(self, mode=False): + """ + Allow ports to be removable on this node. + + See Also: + :meth:`BaseNode.port_deletion_allowed` and + :meth:`BaseNode.set_ports` + + Args: + mode (bool): true to allow. + """ + self.model.port_deletion_allowed = mode + + def port_deletion_allowed(self): + """ + Return true if ports can be deleted on this node. + + See Also: + :meth:`BaseNode.set_port_deletion_allowed` + + Returns: + bool: true if ports can be deleted. + """ + return self.model.port_deletion_allowed + + def set_ports(self, port_data): + """ + Create node input and output ports from serialized port data. + + Warnings: + You can only use this function if the node has + :meth:`BaseNode.port_deletion_allowed` is `True` + see :meth:`BaseNode.set_port_deletion_allowed` + + Hint: + example snippet of port data. + + .. highlight:: python + .. code-block:: python + + { + 'input_ports': + [{ + 'name': 'input', + 'multi_connection': True, + 'display_name': 'Input', + 'locked': False + }], + 'output_ports': + [{ + 'name': 'output', + 'multi_connection': True, + 'display_name': 'Output', + 'locked': False + }] + } + + Args: + port_data(dict): port data. + """ + if not self.port_deletion_allowed(): + raise PortError( + 'Ports cannot be set on this node because ' + '"set_port_deletion_allowed" is not enabled on this node.') + + for port in self._inputs: + self._view.delete_input(port.view) + port.model.node = None + for port in self._outputs: + self._view.delete_output(port.view) + port.model.node = None + self._inputs = [] + self._outputs = [] + self._model.outputs = {} + self._model.inputs = {} + + [self.add_input(name=port['name'], + multi_input=port['multi_connection'], + display_name=port['display_name'], + locked=port.get('locked') or False) + for port in port_data['input_ports']] + [self.add_output(name=port['name'], + multi_output=port['multi_connection'], + display_name=port['display_name'], + locked=port.get('locked') or False) + for port in port_data['output_ports']] + self._view.draw_node() + + def inputs(self): + """ + Returns all the input ports from the node. + + Returns: + dict: {: } + """ + return {p.name(): p for p in self._inputs} + + def input_ports(self): + """ + Return all input ports. + + Returns: + list[NodeGraphQt.Port]: node input ports. + """ + return self._inputs + + def outputs(self): + """ + Returns all the output ports from the node. + + Returns: + dict: {: } + """ + return {p.name(): p for p in self._outputs} + + def output_ports(self): + """ + Return all output ports. + + Returns: + list[NodeGraphQt.Port]: node output ports. + """ + return self._outputs + + def input(self, index): + """ + Return the input port with the matching index. + + Args: + index (int): index of the input port. + + Returns: + NodeGraphQt.Port: port object. + """ + return self._inputs[index] + + def set_input(self, index, port): + """ + Creates a connection pipe to the targeted output :class:`Port`. + + Args: + index (int): index of the port. + port (NodeGraphQt.Port): port object. + """ + src_port = self.input(index) + src_port.connect_to(port) + + def output(self, index): + """ + Return the output port with the matching index. + + Args: + index (int): index of the output port. + + Returns: + NodeGraphQt.Port: port object. + """ + return self._outputs[index] + + def set_output(self, index, port): + """ + Creates a connection pipe to the targeted input :class:`Port`. + + Args: + index (int): index of the port. + port (NodeGraphQt.Port): port object. + """ + src_port = self.output(index) + src_port.connect_to(port) + + def connected_input_nodes(self): + """ + Returns all nodes connected from the input ports. + + Returns: + dict: {: } + """ + nodes = OrderedDict() + for p in self.input_ports(): + nodes[p] = [cp.node() for cp in p.connected_ports()] + return nodes + + def connected_output_nodes(self): + """ + Returns all nodes connected from the output ports. + + Returns: + dict: {: } + """ + nodes = OrderedDict() + for p in self.output_ports(): + nodes[p] = [cp.node() for cp in p.connected_ports()] + return nodes + + def add_accept_port_type(self, port, port_type_data): + """ + Add an accept constrain to a specified node port. + + Once a constraint has been added only ports of that type specified will + be allowed a pipe connection. + + port type data example + + .. highlight:: python + .. code-block:: python + + { + 'port_name': 'foo' + 'port_type': PortTypeEnum.IN.value + 'node_type': 'io.github.jchanvfx.NodeClass' + } + + See Also: + :meth:`NodeGraphQt.BaseNode.accepted_port_types` + + Args: + port (NodeGraphQt.Port): port to assign constrain to. + port_type_data (dict): port type data to accept a connection + """ + node_ports = self._inputs + self._outputs + if port not in node_ports: + raise PortError('Node does not contain port: "{}"'.format(port)) + + self._model.add_port_accept_connection_type( + port_name=port.name(), + port_type=port.type_(), + node_type=self.type_, + accept_pname=port_type_data['port_name'], + accept_ptype=port_type_data['port_type'], + accept_ntype=port_type_data['node_type'] + ) + + def accepted_port_types(self, port): + """ + Returns a dictionary of connection constrains of the port types + that allow for a pipe connection to this node. + + Args: + port (NodeGraphQt.Port): port object. + + Returns: + dict: {: {: []}} + """ + ports = self._inputs + self._outputs + if port not in ports: + raise PortError('Node does not contain port "{}"'.format(port)) + + accepted_types = self.graph.model.port_accept_connection_types( + node_type=self.type_, + port_type=port.type_(), + port_name=port.name() + ) + return accepted_types + + def add_reject_port_type(self, port, port_type_data): + """ + Add a reject constrain to a specified node port. + + Once a constraint has been added only ports of that type specified will + NOT be allowed a pipe connection. + + port type data example + + .. highlight:: python + .. code-block:: python + + { + 'port_name': 'foo' + 'port_type': PortTypeEnum.IN.value + 'node_type': 'io.github.jchanvfx.NodeClass' + } + + See Also: + :meth:`NodeGraphQt.Port.rejected_port_types` + + Args: + port (NodeGraphQt.Port): port to assign constrain to. + port_type_data (dict): port type data to reject a connection + """ + node_ports = self._inputs + self._outputs + if port not in node_ports: + raise PortError('Node does not contain port: "{}"'.format(port)) + + self._model.add_port_reject_connection_type( + port_name=port.name(), + port_type=port.type_(), + node_type=self.type_, + reject_pname=port_type_data['port_name'], + reject_ptype=port_type_data['port_type'], + reject_ntype=port_type_data['node_type'] + ) + + def rejected_port_types(self, port): + """ + Returns a dictionary of connection constrains of the port types + that are NOT allowed for a pipe connection to this node. + + Args: + port (NodeGraphQt.Port): port object. + + Returns: + dict: {: {: []}} + """ + ports = self._inputs + self._outputs + if port not in ports: + raise PortError('Node does not contain port "{}"'.format(port)) + + rejected_types = self.graph.model.port_reject_connection_types( + node_type=self.type_, + port_type=port.type_(), + port_name=port.name() + ) + return rejected_types + + def on_input_connected(self, in_port, out_port): + """ + Callback triggered when a new pipe connection is made. + + *The default of this function does nothing re-implement if you require + logic to run for this event.* + + Note: + to work with undo & redo for this method re-implement + :meth:`BaseNode.on_input_disconnected` with the reverse logic. + + Args: + in_port (NodeGraphQt.Port): source input port from this node. + out_port (NodeGraphQt.Port): output port that connected to this node. + """ + return + + def on_input_disconnected(self, in_port, out_port): + """ + Callback triggered when a pipe connection has been disconnected + from a INPUT port. + + *The default of this function does nothing re-implement if you require + logic to run for this event.* + + Note: + to work with undo & redo for this method re-implement + :meth:`BaseNode.on_input_connected` with the reverse logic. + + Args: + in_port (NodeGraphQt.Port): source input port from this node. + out_port (NodeGraphQt.Port): output port that was disconnected. + """ + return \ No newline at end of file diff --git a/Nodes/character_status_node.py b/Nodes/character_status_node.py index 62eb026..5e12394 100644 --- a/Nodes/character_status_node.py +++ b/Nodes/character_status_node.py @@ -44,7 +44,7 @@ def get_draw_stat_port(color, border_color=None, alpha=127): return painter_func class CharacterStatusNode(BaseNode): - __identifier__ = 'io.github.nicole.status' + __identifier__ = 'bunny-lab.io.status_node' NODE_NAME = 'Character Status' def __init__(self): diff --git a/Nodes/convert_to_percent_node.py b/Nodes/convert_to_percent_node.py deleted file mode 100644 index ea7d127..0000000 --- a/Nodes/convert_to_percent_node.py +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env python3 -""" -Convert to Percent Node - -This node takes two numerical inputs (A and B), computes (A / B) * 100, -and outputs the result as a float formatted to 4 decimal places. If an error -occurs, an error message is stored. The node's title is always "Convert to Percent". -""" - -from NodeGraphQt import BaseNode -from Qt import QtCore - -class ConvertToPercentNode(BaseNode): - __identifier__ = 'io.github.nicole.convert' - NODE_NAME = 'Convert to Percent' - - def __init__(self): - super(ConvertToPercentNode, self).__init__() - # Add two input ports for separate numerator and denominator. - self.add_input("Numerator") - self.add_input("Denominator") - # Add one output port. - self.add_output("Percent") - # Initialize internal value. - self.value = "No Input" - # Set the node title to a static string. - self.set_name(self.NODE_NAME) - # Initialize a values dictionary so that connected Display nodes can read output. - self.values = {} - - def process_input(self): - numerator_input = self.input(0) - denominator_input = self.input(1) - - numerator = self.get_connected_value(numerator_input) - denominator = self.get_connected_value(denominator_input) - - try: - numerator = float(numerator) - denominator = float(denominator) - if denominator == 0: - raise ZeroDivisionError("Division by zero") - percent = (numerator / denominator) * 100 - formatted_percent = f"{percent:.4f}" - self.value = formatted_percent - except Exception as e: - self.value = f"Error: {e}" - - # Always keep the title static. - self.set_name(self.NODE_NAME) - # Store the computed value in the values dictionary under the output port key. - self.values["Percent"] = self.value - - def get_connected_value(self, input_port): - """ - Helper function to retrieve the value from a connected port. - """ - if input_port and input_port.connected_ports(): - connected_output = input_port.connected_ports()[0] - parent_node = connected_output.node() - port_name = connected_output.name() - if hasattr(parent_node, 'values') and isinstance(parent_node.values, dict): - return parent_node.values.get(port_name, "0") - return getattr(parent_node, 'value', "0") - return "0" diff --git a/Nodes/data_node.py b/Nodes/data_node.py index a63b630..7c89dc1 100644 --- a/Nodes/data_node.py +++ b/Nodes/data_node.py @@ -18,7 +18,7 @@ Behavior: from NodeGraphQt import BaseNode class DataNode(BaseNode): - __identifier__ = 'io.github.nicole.data' + __identifier__ = 'bunny-lab.io.data_node' NODE_NAME = 'Data Node' def __init__(self): @@ -50,7 +50,6 @@ class DataNode(BaseNode): """ current_text = self.get_property('value') self.value = current_text - self.set_name(f"Data Node") def property_changed(self, property_name): """ @@ -114,7 +113,6 @@ class DataNode(BaseNode): Receives data from connected nodes and updates the internal value. """ self.set_property('value', str(data)) # Ensure it's always stored as a string - self.set_name(f"Data Node") # Transmit data further if there's an output connection output_port = self.output(0) diff --git a/Nodes/group_node.py b/Nodes/group_node.py new file mode 100644 index 0000000..f87fe32 --- /dev/null +++ b/Nodes/group_node.py @@ -0,0 +1,176 @@ +#!/usr/bin/python +from NodeGraphQt.nodes.base_node import BaseNode +from NodeGraphQt.nodes.port_node import PortInputNode, PortOutputNode +from NodeGraphQt.qgraphics.node_group import GroupNodeItem + + +class GroupNode(BaseNode): + """ + `Implemented in` ``v0.2.0`` + + The ``NodeGraphQt.GroupNode`` class extends from the :class:`NodeGraphQt.BaseNode` + class with the ability to nest other nodes inside of it. + + .. inheritance-diagram:: NodeGraphQt.GroupNode + + .. image:: ../_images/group_node.png + :width: 250px + + - + """ + + NODE_NAME = 'Group' + + def __init__(self, qgraphics_item=None): + super(GroupNode, self).__init__(qgraphics_item or GroupNodeItem) + self._input_port_nodes = {} + self._output_port_nodes = {} + + @property + def is_expanded(self): + """ + Returns if the group node is expanded or collapsed. + + Returns: + bool: true if the node is expanded. + """ + if not self.graph: + return False + return bool(self.id in self.graph.sub_graphs) + + def get_sub_graph(self): + """ + Returns the sub graph controller to the group node if initialized + or returns None. + + Returns: + SubGraph: sub graph controller. + """ + return self.graph.sub_graphs.get(self.id) + + def get_sub_graph_session(self): + """ + Returns the serialized sub graph session. + + Returns: + dict: serialized sub graph session. + """ + return self.model.subgraph_session + + def set_sub_graph_session(self, serialized_session): + """ + Sets the sub graph session data to the group node. + + Args: + serialized_session (dict): serialized session. + """ + serialized_session = serialized_session or {} + self.model.subgraph_session = serialized_session + + def expand(self): + """ + Expand the group node session. + + See Also: + :meth:`NodeGraph.expand_group_node`, + :meth:`SubGraph.expand_group_node`. + + Returns: + SubGraph: node graph used to manage the nodes expaneded session. + """ + sub_graph = self.graph.expand_group_node(self) + return sub_graph + + def collapse(self): + """ + Collapse the group node session it's expanded child sub graphs. + + See Also: + :meth:`NodeGraph.collapse_group_node`, + :meth:`SubGraph.collapse_group_node`. + """ + self.graph.collapse_group_node(self) + + def set_name(self, name=''): + super(GroupNode, self).set_name(name) + # update the tab bar and navigation labels. + sub_graph = self.get_sub_graph() + if sub_graph: + nav_widget = sub_graph.navigation_widget + nav_widget.update_label_item(self.name(), self.id) + + if sub_graph.parent_graph.is_root: + root_graph = sub_graph.parent_graph + tab_bar = root_graph.widget.tabBar() + for idx in range(tab_bar.count()): + if tab_bar.tabToolTip(idx) == self.id: + tab_bar.setTabText(idx, self.name()) + break + + def add_input(self, name='input', multi_input=False, display_name=True, + color=None, locked=False, painter_func=None): + port = super(GroupNode, self).add_input( + name=name, + multi_input=multi_input, + display_name=display_name, + color=color, + locked=locked, + painter_func=painter_func + ) + if self.is_expanded: + input_node = PortInputNode(parent_port=port) + input_node.NODE_NAME = port.name() + input_node.model.set_property('name', port.name()) + input_node.add_output(port.name()) + sub_graph = self.get_sub_graph() + sub_graph.add_node(input_node, selected=False, push_undo=False) + + return port + + def add_output(self, name='output', multi_output=True, display_name=True, + color=None, locked=False, painter_func=None): + port = super(GroupNode, self).add_output( + name=name, + multi_output=multi_output, + display_name=display_name, + color=color, + locked=locked, + painter_func=painter_func + ) + if self.is_expanded: + output_port = PortOutputNode(parent_port=port) + output_port.NODE_NAME = port.name() + output_port.model.set_property('name', port.name()) + output_port.add_input(port.name()) + sub_graph = self.get_sub_graph() + sub_graph.add_node(output_port, selected=False, push_undo=False) + + return port + + def delete_input(self, port): + if type(port) in [int, str]: + port = self.get_input(port) + if port is None: + return + + if self.is_expanded: + sub_graph = self.get_sub_graph() + port_node = sub_graph.get_node_by_port(port) + if port_node: + sub_graph.remove_node(port_node, push_undo=False) + + super(GroupNode, self).delete_input(port) + + def delete_output(self, port): + if type(port) in [int, str]: + port = self.get_output(port) + if port is None: + return + + if self.is_expanded: + sub_graph = self.get_sub_graph() + port_node = sub_graph.get_node_by_port(port) + if port_node: + sub_graph.remove_node(port_node, push_undo=False) + + super(GroupNode, self).delete_output(port) \ No newline at end of file diff --git a/Nodes/math_operation_node.py b/Nodes/math_operation_node.py new file mode 100644 index 0000000..2bf84e4 --- /dev/null +++ b/Nodes/math_operation_node.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +""" +Math Operation Node: + - Inputs: Two input ports ("A" and "B"). + - Output: One output port ("Result"). + - Operation: A dropdown (combo menu) to select: + Add, Subtract, Multiply, Divide, Average. + - Displays the computed result in a read-only text box labeled "Result". +""" + +from NodeGraphQt import BaseNode + +class MathOperationNode(BaseNode): + __identifier__ = 'bunny-lab.io.math_node' + NODE_NAME = 'Math Operation' + + def __init__(self): + super(MathOperationNode, self).__init__() + + # ---------------------------------------------------------------------- + # Initialization Section: + # - Create two input ports: A, B + # - Create one output port: Result + # - Add a combo box for operator selection + # - Add a text input for displaying the computed result + # ---------------------------------------------------------------------- + self.add_input('A') + self.add_input('B') + self.add_output('Result') + + # Operator combo box (Add, Subtract, Multiply, Divide, Average) + self.add_combo_menu('operator', 'Operator', items=[ + 'Add', 'Subtract', 'Multiply', 'Divide', 'Average' + ]) + + # Text input for displaying the computed result. + # We'll make it read-only by accessing the underlying QLineEdit. + self.add_text_input('calc_result', 'Result', text='0') + result_widget = self.get_widget('calc_result') # This is a NodeLineEdit wrapper + if result_widget: + # Get the underlying QLineEdit + line_edit = result_widget.get_custom_widget() + # Make the QLineEdit read-only + line_edit.setReadOnly(True) + + self.value = 0 + self.set_name("Math Operation") + self.process_input() + + def process_input(self, event=None): + """ + Compute Section: + - For each input port (A, B), if connected, grab the 'value' from + the upstream node; otherwise default to 0.0. + - Convert to float, apply the selected operation from the combo box, + update the "Result" text box, node title, and output port. + """ + # Gather input A + input_a = self.input(0) + if input_a and input_a.connected_ports(): + a_raw = input_a.connected_ports()[0].node().get_property('value') + else: + a_raw = 0.0 + + # Gather input B + input_b = self.input(1) + if input_b and input_b.connected_ports(): + b_raw = input_b.connected_ports()[0].node().get_property('value') + else: + b_raw = 0.0 + + # Convert raw inputs to floats (default 0.0 on failure). + try: + a_val = float(a_raw) + except (ValueError, TypeError): + a_val = 0.0 + try: + b_val = float(b_raw) + except (ValueError, TypeError): + b_val = 0.0 + + # Retrieve the selected operator from the combo box. + operator = self.get_property('operator') + result = 0.0 + + if operator == 'Add': + result = a_val + b_val + elif operator == 'Subtract': + result = a_val - b_val + elif operator == 'Multiply': + result = a_val * b_val + elif operator == 'Divide': + result = a_val / b_val if b_val != 0 else 0.0 + elif operator == 'Average': + result = (a_val + b_val) / 2.0 + + self.value = result + + # Update the read-only text input and node title. + self.set_property('calc_result', str(result)) + + # Transmit the numeric result to any connected output nodes. + output_port = self.output(0) + if output_port and output_port.connected_ports(): + for cp in output_port.connected_ports(): + connected_node = cp.node() + if hasattr(connected_node, 'receive_data'): + connected_node.receive_data(result, source_port_name='Result') + + def on_input_connected(self, input_port, output_port): + self.process_input() + + def on_input_disconnected(self, input_port, output_port): + self.process_input() + + def property_changed(self, property_name): + if property_name in ['operator']: + self.process_input() + + def receive_data(self, data, source_port_name=None): + self.process_input() diff --git a/Nodes/multiply_node.py b/Nodes/multiply_node.py deleted file mode 100644 index c5211c9..0000000 --- a/Nodes/multiply_node.py +++ /dev/null @@ -1,36 +0,0 @@ -from NodeGraphQt import BaseNode - -class MultiplyNode(BaseNode): - """ - Multiply Node: - - Inputs: A, B - - Output: Result (A * B) - """ - __identifier__ = 'io.github.nicole.multiply' - NODE_NAME = 'Multiply' - - def __init__(self): - super(MultiplyNode, self).__init__() - self.values = {} # Ensure values is a dictionary. - self.add_input('A') - self.add_input('B') - self.add_output('Result') - self.value = 0 - - def process_input(self): - inputs = {} - for port_name in ['A', 'B']: - port = self.input(port_name) - connected = port.connected_ports() if port is not None else [] - if connected: - connected_port = connected[0] - parent_node = connected_port.node() - try: - inputs[port_name] = float(getattr(parent_node, 'value', 0)) - except (ValueError, TypeError): - inputs[port_name] = 0.0 - else: - inputs[port_name] = 0.0 - result = inputs['A'] * inputs['B'] - self.value = result - self.set_name(f"Multiply: {result}") diff --git a/Nodes/port_node.py b/Nodes/port_node.py new file mode 100644 index 0000000..6a71524 --- /dev/null +++ b/Nodes/port_node.py @@ -0,0 +1,135 @@ +#!/usr/bin/python +from NodeGraphQt.errors import PortRegistrationError +from NodeGraphQt.nodes.base_node import BaseNode +from NodeGraphQt.qgraphics.node_port_in import PortInputNodeItem +from NodeGraphQt.qgraphics.node_port_out import PortOutputNodeItem + + +class PortInputNode(BaseNode): + """ + The ``PortInputNode`` is the node that represents a input port from a + :class:`NodeGraphQt.GroupNode` when expanded in a + :class:`NodeGraphQt.SubGraph`. + + .. inheritance-diagram:: NodeGraphQt.nodes.port_node.PortInputNode + :parts: 1 + + .. image:: ../_images/port_in_node.png + :width: 150px + + - + """ + + NODE_NAME = 'InputPort' + + def __init__(self, qgraphics_item=None, parent_port=None): + super(PortInputNode, self).__init__(qgraphics_item or PortInputNodeItem) + self._parent_port = parent_port + + @property + def parent_port(self): + """ + The parent group node port representing this node. + + Returns: + NodeGraphQt.Port: port object. + """ + return self._parent_port + + def add_input(self, name='input', multi_input=False, display_name=True, + color=None, locked=False, painter_func=None): + """ + Warnings: + This is not available for the ``PortInputNode`` class. + """ + raise PortRegistrationError( + '"{}.add_input()" is not available for {}.' + .format(self.__class__.__name__, self) + ) + + def add_output(self, name='output', multi_output=True, display_name=True, + color=None, locked=False, painter_func=None): + """ + Warnings: + This function is called by :meth:`NodeGraphQt.SubGraph.expand_group_node` + and is not available for the ``PortInputNode`` class. + """ + if self._outputs: + raise PortRegistrationError( + '"{}.add_output()" only ONE output is allowed for this node.' + .format(self.__class__.__name__, self) + ) + super(PortInputNode, self).add_output( + name=name, + multi_output=multi_output, + display_name=False, + color=color, + locked=locked, + painter_func=None + ) + + +class PortOutputNode(BaseNode): + """ + The ``PortOutputNode`` is the node that represents a output port from a + :class:`NodeGraphQt.GroupNode` when expanded in a + :class:`NodeGraphQt.SubGraph`. + + .. inheritance-diagram:: NodeGraphQt.nodes.port_node.PortOutputNode + :parts: 1 + + .. image:: ../_images/port_out_node.png + :width: 150px + + - + """ + + NODE_NAME = 'OutputPort' + + def __init__(self, qgraphics_item=None, parent_port=None): + super(PortOutputNode, self).__init__( + qgraphics_item or PortOutputNodeItem + ) + self._parent_port = parent_port + + @property + def parent_port(self): + """ + The parent group node port representing this node. + + Returns: + NodeGraphQt.Port: port object. + """ + return self._parent_port + + def add_input(self, name='input', multi_input=False, display_name=True, + color=None, locked=False, painter_func=None): + """ + Warnings: + This function is called by :meth:`NodeGraphQt.SubGraph.expand_group_node` + and is not available for the ``PortOutputNode`` class. + """ + if self._inputs: + raise PortRegistrationError( + '"{}.add_input()" only ONE input is allowed for this node.' + .format(self.__class__.__name__, self) + ) + super(PortOutputNode, self).add_input( + name=name, + multi_input=multi_input, + display_name=False, + color=color, + locked=locked, + painter_func=None + ) + + def add_output(self, name='output', multi_output=True, display_name=True, + color=None, locked=False, painter_func=None): + """ + Warnings: + This is not available for the ``PortOutputNode`` class. + """ + raise PortRegistrationError( + '"{}.add_output()" is not available for {}.' + .format(self.__class__.__name__, self) + ) \ No newline at end of file diff --git a/Nodes/subtract_node.py b/Nodes/subtract_node.py deleted file mode 100644 index 08d37af..0000000 --- a/Nodes/subtract_node.py +++ /dev/null @@ -1,37 +0,0 @@ -# Nodes/subtract_node.py - -from NodeGraphQt import BaseNode - -class SubtractNode(BaseNode): - """ - Subtract Node: - - Inputs: A, B - - Output: Result (A - B) - """ - __identifier__ = 'io.github.nicole.subtract' - NODE_NAME = 'Subtract' - - def __init__(self): - super(SubtractNode, self).__init__() - self.add_input('A') - self.add_input('B') - self.add_output('Result') - self.value = 0 - - def process_input(self): - inputs = {} - for port_name in ['A', 'B']: - port = self.input(port_name) - connected = port.connected_ports() if port is not None else [] - if connected: - connected_port = connected[0] - parent_node = connected_port.node() - try: - inputs[port_name] = float(getattr(parent_node, 'value', 0)) - except (ValueError, TypeError): - inputs[port_name] = 0.0 - else: - inputs[port_name] = 0.0 - result = inputs['A'] - inputs['B'] - self.value = result - self.set_name(f"Subtract: {result}") diff --git a/debug_processed.png b/debug_processed.png index 981699cad762ca7162b50642684564241725b1ee..6a2aa34d576df3a9734f035fe725bef939c4f72e 100644 GIT binary patch delta 233 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 H@jm!J_A942 literal 13623 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