├── LICENSE ├── README.md ├── examples ├── 3DExample │ ├── ctk_spinbox.py │ ├── cube.json │ ├── pyramid.json │ ├── render_engine.py │ ├── requirements.txt │ └── threeDapp.py ├── advanced_example.py ├── maths_with_tknodes.py └── simple_example.py └── tknodesystem ├── __init__.py ├── grid_images ├── grid_dot.png ├── grid_lines.png └── no_grid.png ├── node.py ├── node_args.py ├── node_canvas.py ├── node_menu.py ├── node_socket.py ├── node_types.py └── node_wire.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Akash Bora 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![title](https://github.com/Akascape/TkNodeSystem/assets/89206401/a84cb60a-cc6f-4609-8452-3ff4fcd0c46b) 2 | 3 | **Advanced Node System (DAG) in tkinter python!** 4 | 5 | ## Feature 6 | - Lightweight library 7 | - Easy to install and use 8 | - Multiple nodes and inputs 9 | - **Save/Load** node trees 10 | - Canvas with **drag and zoom** ability 11 | - Customizable nodes and options 12 | - Built-in **Node menu** 13 | 14 | ## Installation 15 | ``` 16 | pip install tknodesystem 17 | ``` 18 | ### [GitHub repo size](https://github.com/Akascape/TkNodeSystem/archive/refs/heads/main.zip) 19 | 20 | [![PyPI](https://img.shields.io/pypi/v/tknodesystem)](https://pypi.org/project/tknodesystem) 21 | [![Downloads](https://static.pepy.tech/badge/tknodesystem)](https://pepy.tech/project/tknodesystem) 22 | ![Platform](https://img.shields.io/powershellgallery/p/Pester?color=blue) 23 | 24 | ### Requirements 25 | - tkinter 26 | - customtkinter _(for the node menu)_ 27 | 28 | ## Overview 29 | 30 | - Node Types 31 | 32 | ![node_types](https://github.com/Akascape/TkNodeSystem/assets/89206401/cccf82dd-8207-4894-9e9e-ef240e511d85) 33 | 34 | - Node Menu 35 | 36 | ![node_menu](https://github.com/Akascape/TkNodeSystem/assets/89206401/0ba5ba42-4787-4b94-8b40-682084df2e80) 37 | 38 | - Node Canvas 39 | 40 | ![canvas_types](https://github.com/Akascape/TkNodeSystem/assets/89206401/d5568962-50c0-404c-bf71-79d66f79e3b7) 41 | 42 | ## Documentation 43 | Full documentation can be found in the **Wiki** page 44 | 45 | [](https://github.com/Akascape/TkNodeSystem/wiki) 46 | 47 | ## Examples 48 | Examples can be found in the [`examples`](https://github.com/Akascape/TkNodeSystem/tree/main/examples) folder. 49 | 50 |
51 | 52 | ### Level: Beginner 53 | Maths with tknodes 54 | 55 | ![Example1](https://github.com/Akascape/TkNodeSystem/assets/89206401/9bb709fa-78e8-4db0-b5f1-8848fd32aa81) 56 | 57 | ### Level: Intermediate 58 | Image manipulation with PIL 59 | 60 | ![Example2](https://github.com/Akascape/TkNodeSystem/assets/89206401/ea818333-c979-4402-bc7c-8850930dc087) 61 | 62 | ### Level: Advanced 63 | 3D Viewer 64 | 65 | ![Screenshot](https://github.com/Akascape/TkNodeSystem/assets/89206401/83e5d356-947e-4737-9f92-487a2cfc7d3f) 66 | ![Screenshot](https://github.com/Akascape/TkNodeSystem/assets/89206401/a5695e72-c739-41ce-a700-20c3b4bd052b) 67 | 68 |
69 | 70 | ### Bug Fixes 71 | This library is at **experimental stage**, so there must be some bugs which can appear randomly. 72 | 73 | **So, please report the bugs at the issues/discussions tab. A pull request is always welcomed :)** 74 | -------------------------------------------------------------------------------- /examples/3DExample/ctk_spinbox.py: -------------------------------------------------------------------------------- 1 | # Author: Akash Bora (Akascape) 2 | # License: MIT 3 | 4 | import customtkinter 5 | import tkinter 6 | from typing import Union, Callable 7 | 8 | class CTkSpinbox(customtkinter.CTkFrame): 9 | 10 | def __init__(self, *args, 11 | width: int = 100, 12 | height: int = 32, 13 | number_of_steps: int = 1, 14 | from_: int = 0, 15 | to: int = 1000, 16 | value: int = None, 17 | button_color: str = None, 18 | button_width: int = 25, 19 | button_hover_color: str = None, 20 | entry_color: str = None, 21 | text_color: str = None, 22 | border_color: str = None, 23 | command: Callable = None, 24 | border_width: int = 0, 25 | corner_radius: int = 5, 26 | font = None, 27 | **kwargs): 28 | 29 | super().__init__(*args, height=height, **kwargs) 30 | 31 | self.step_size = number_of_steps 32 | self.max_value = to 33 | self.min_value = from_ 34 | self.command = command 35 | self.validation = self.register(self.only_numbers) 36 | self.value = value 37 | 38 | self.grid_columnconfigure((0, 2), weight=0) 39 | self.grid_columnconfigure(1, weight=1) 40 | 41 | self.button_color = customtkinter.ThemeManager.theme["CTkButton"]["fg_color"] if button_color is None else button_color 42 | self.button_hover = customtkinter.ThemeManager.theme["CTkButton"]["hover_color"] if button_hover_color is None else button_hover_color 43 | self.entry_color = customtkinter.ThemeManager.theme["CTkEntry"]["fg_color"] if entry_color is None else entry_color 44 | self.border_color = customtkinter.ThemeManager.theme["CTkEntry"]["border_color"] if border_color is None else border_color 45 | self.text_color = customtkinter.ThemeManager.theme["CTkEntry"]["text_color"] if text_color is None else text_color 46 | self.border_width = border_width 47 | self.corner_radius = corner_radius 48 | self.button_width = button_width 49 | self.font = font 50 | 51 | super().configure(border_color=self.border_color) 52 | 53 | self.subtract_button = customtkinter.CTkButton(self, text="-", width=self.button_width, height=height-6, corner_radius=self.corner_radius, 54 | border_color=self.border_color, text_color=self.text_color, font=self.font, 55 | command=self.subtract_button_callback, fg_color=self.button_color, 56 | hover_color=self.button_hover, border_width=self.border_width) 57 | self.subtract_button.grid(row=0, column=0, padx=(3, 0), pady=3) 58 | 59 | self.text = tkinter.StringVar() 60 | self.text.set(self.min_value) 61 | 62 | self.entry = customtkinter.CTkEntry(self, width=width, height=height-6, textvariable=self.text, font=self.font, 63 | fg_color=self.entry_color, border_width=self.border_width+2, 64 | placeholder_text=str(self.min_value), justify="center", validate='key', 65 | border_color=self.border_color, corner_radius=self.corner_radius, 66 | validatecommand=(self.validation, '%P'), text_color=self.text_color) 67 | self.entry.grid(row=0, column=1, columnspan=1, padx=3, pady=3, sticky="ew") 68 | 69 | self.add_button = customtkinter.CTkButton(self, text="+", width=self.button_width, height=height-6, corner_radius=self.corner_radius, 70 | border_color=self.border_color, text_color=self.text_color, font=self.font, 71 | command=self.add_button_callback, fg_color=self.button_color, 72 | hover_color=self.button_hover, border_width=self.border_width) 73 | self.add_button.grid(row=0, column=2, padx=(0, 3), pady=3) 74 | self.entry.bind("", self.on_mouse_wheel) 75 | 76 | if self.value is None: 77 | self.set(self.min_value) 78 | else: 79 | self.set(self.value) 80 | 81 | def on_mouse_wheel(self, event): 82 | if event.delta > 0: 83 | self.add_button_callback() 84 | else: 85 | self.subtract_button_callback() 86 | 87 | def add_button_callback(self): 88 | if self.entry.get()=="": 89 | self.set(0) 90 | return 91 | 92 | self.set(int(self.entry.get()) + self.step_size) 93 | 94 | if self.command is not None: 95 | self.command() 96 | 97 | def subtract_button_callback(self): 98 | if self.entry.get()=="": 99 | self.set(0) 100 | return 101 | 102 | self.set(int(self.entry.get()) - self.step_size) 103 | 104 | if self.command is not None: 105 | self.command() 106 | 107 | def configure(self, **kwargs): 108 | 109 | if "state" in kwargs: 110 | if kwargs["state"]=="disabled": 111 | self.entry.unbind("") 112 | else: 113 | self.entry.bind("", self.on_mouse_wheel) 114 | super().configure(state=kwargs["state"]) 115 | 116 | if "width" in kwargs: 117 | self.entry.configure(width=kwargs.pop("width")) 118 | if "fg_color" in kwargs: 119 | super().configure(fg_color=kwargs.pop("fg_color")) 120 | if "corner_radius" in kwargs: 121 | self.corner_radius = kwargs["corner_radius"] 122 | if "border_width" in kwargs: 123 | self.border_width = kwargs.pop("border_width") 124 | self.entry.configure(border_width=self.border_width+2) 125 | self.add_button.configure(border_width=self.border_width) 126 | self.subtract_button.configure(border_width=self.border_width) 127 | if "button_width" in kwargs: 128 | self.button_width = kwargs.pop("button_width") 129 | self.add_button.configure(width=self.button_width) 130 | self.subtract_button.configure(width=self.button_width) 131 | if "border_color" in kwargs: 132 | self.border_color = kwargs["border_color"] 133 | if "button_color" in kwargs: 134 | self.button_color = kwargs.pop("button_color") 135 | self.add_button.configure(fg_color=self.button_color) 136 | self.subtract_button.configure(fg_color=self.button_color) 137 | if "button_hover_color" in kwargs: 138 | self.button_hover = kwargs.pop("button_hover_color") 139 | self.add_button.configure(hover_color=self.button_hover) 140 | self.subtract_button.configure(hover_color=self.button_hover) 141 | if "entry_color" in kwargs: 142 | self.entry_color = kwargs.pop("entry_color") 143 | self.entry.configure(fg_color=self.entry_color) 144 | if "text_color" in kwargs: 145 | self.text_color = kwargs["text_color"] 146 | if "value" in kwargs: 147 | self.values = kwargs.pop("value") 148 | self.set(self.values) 149 | if "from_" in kwargs: 150 | self.min_value = kwargs.pop("from_") 151 | if int(self.value)self.max_value: 156 | self.set(self.max_value) 157 | if "number_of_steps" in kwargs: 158 | self.step_size = kwargs.pop("number_of_steps") 159 | if "command" in kwargs: 160 | self.command = kwargs.pop("command") 161 | if "font" in kwargs: 162 | self.font = kwargs["font"] 163 | 164 | self.entry.configure(**kwargs) 165 | self.add_button.configure(**kwargs) 166 | self.subtract_button.configure(**kwargs) 167 | 168 | def cget(self, param): 169 | if param=="width": 170 | return self.entry.winfo_width() 171 | if param=="height": 172 | return super().winfo_height() 173 | if param=="corner_radius": 174 | return self.corner_radius 175 | if param=="border_width": 176 | return self.border_width 177 | if param=="button_width": 178 | return self.button_width 179 | if param=="border_color": 180 | return self.border_color 181 | if param=="button_color": 182 | return self.button_color 183 | if param=="button_hover_color": 184 | return self.button_hover 185 | if param=="entry_color": 186 | return self.entry_color 187 | if param=="text_color": 188 | return self.text_color 189 | if param=="value": 190 | return int(self.entry.get()) 191 | if param=="from_": 192 | return self.min_value 193 | if param=="to": 194 | return self.max_value 195 | if param=="number_of_steps": 196 | return self.step_size 197 | if param=="font": 198 | return self.font 199 | return super().cget(param) 200 | 201 | def only_numbers(self, char): 202 | if (char.isdigit() or (char=="")): 203 | if (len(str(self.max_value))-1)==str(self.max_value).count("0"): 204 | if (len(char)<=len(str(self.max_value))-1) or int(char)==self.max_value: 205 | return True 206 | else: 207 | return False 208 | else: 209 | if (len(char)<=len(str(self.max_value))): 210 | return True 211 | else: 212 | return False 213 | else: 214 | return False 215 | 216 | def get(self) -> Union[int, None]: 217 | if self.entry.get()=="": 218 | return 0 219 | try: 220 | return int(self.text.get()) 221 | except ValueError: 222 | return None 223 | 224 | def set(self, value: int): 225 | if int(value)>self.max_value: 226 | self.set(self.max_value) 227 | return 228 | if int(value)self.d: 114 | self.d = distance 115 | self.d*=50 116 | 117 | def set_view_point(self, view_point = None, reference_point = None): 118 | if view_point == None and reference_point == None: 119 | self.a = m.cos(self.beeta) * m.sin(self.alpha) 120 | self.b = m.sin(self.beeta) 121 | self.c = m.cos(self.beeta) * m.cos(self.alpha) 122 | 123 | reference_point = self.rotate_zaxis((0,1,0), theeta = self.gaama) 124 | self.set_virtual_axis(reference_point) 125 | else: 126 | self.a, self.b, self.c = view_point 127 | self.set_virtual_axis(reference_point) 128 | 129 | def distinct_points(self): 130 | points = [] 131 | for surface in self.coords: 132 | for polygon in surface: 133 | for point in polygon[:-1]: 134 | if point not in points: 135 | points.append(point) 136 | return points 137 | 138 | def set_canvas_size(self): 139 | self.csize = 0 140 | points = self.distinct_points() 141 | for point in points: 142 | distance = self.dist(point,(0,0,0)) 143 | if distance>self.csize: 144 | self.csize = distance 145 | self.csize = int(self.csize*2*self.unit_pixels)+50 146 | #self.canvas.config(width = self.csize, height = self.csize) 147 | 148 | @staticmethod 149 | def plane_equation(point1, point2, point3): 150 | x1,y1,z1 = point1 151 | x2,y2,z2 = point2 152 | x3,y3,z3 = point3 153 | 154 | a = (y2-y1)*(z3-z1)-(y3-y1)*(z2-z1) 155 | b = (x3-x1)*(z2-z1)-(x2-x1)*(z3-z1) 156 | c = (x2-x1)*(y3-y1)-(x3-x1)*(y2-y1) 157 | d = a*x1 + b*y1 + c*z1 158 | 159 | return [a,b,c,d] 160 | 161 | def set_surface_equations(self): 162 | self.s_equations = [] 163 | for surface in self.coords: 164 | point1 = surface[0][0] 165 | point2 = surface[0][1] 166 | point3 = surface[0][2] 167 | 168 | self.s_equations.append(self.plane_equation(point1,point2,point3)) 169 | 170 | for polygon in surface: 171 | for point in polygon[:-1]: 172 | x,y,z = point 173 | a,b,c,d = self.s_equations[-1] 174 | if a*x + b*y + c*z != d: 175 | return 0 176 | 177 | return 1 178 | 179 | def display_list(self): 180 | l = [] 181 | 182 | for equation in self.s_equations: 183 | A,B,C,D = equation 184 | x,y,z = self.d*self.a,self.d*self.b,self.d*self.c 185 | l.append(A*x + B*y + C*z >= D) 186 | 187 | return l 188 | 189 | 190 | 191 | def display_surfaces(self,coords): 192 | d_list = self.display_list() 193 | d_surface = [] 194 | 195 | for i in range(len(d_list)): 196 | if d_list[i]: 197 | d_surface.append(coords[i]) 198 | 199 | return d_surface 200 | 201 | def threeD_to_twoD(self): 202 | return_coords = [] 203 | for surface in self.coords: 204 | return_surface = [] 205 | for polygon in surface: 206 | return_polygon = [] 207 | for point in polygon[:-1]: 208 | x,y,z = point 209 | a,b,c = self.a, self.b, self.c 210 | 211 | X = x*(b**2+c**2) - y*(a*b) - z*(a*c) 212 | Y = y*(a**2+c**2) - z*(b*c) - x*(a*b) 213 | Z = z*(a**2+b**2) - y*(b*c) - x*(a*c) 214 | 215 | lamda = m.sqrt(b**2+c**2) 216 | v = m.sqrt(a**2+b**2+c**2) 217 | if lamda == 0: 218 | lamdax = 1 219 | c=1 220 | else: 221 | lamdax = lamda 222 | 223 | X,Y,Z = self.rotate_xaxis((X,Y,Z), cos_val = c/lamdax, sin_val = b/lamdax) 224 | X,Y,Z = self.rotate_yaxis((X,Y,Z), cos_val = lamda/v, sin_val = -a/v) 225 | 226 | new_vxaxis = self.rotate_xaxis(self.vxaxis, cos_val = c/lamdax, sin_val = b/lamdax) 227 | new_vxaxis = self.rotate_yaxis(new_vxaxis, cos_val = lamda/v, sin_val = -a/v) 228 | 229 | new_referencepoint = self.rotate_xaxis(self.reference_point, cos_val = c/lamdax, sin_val = b/lamdax) 230 | new_referencepoint = self.rotate_yaxis(new_referencepoint, cos_val = lamda/v, sin_val = -a/v) 231 | 232 | if new_vxaxis[1]>=0 and new_referencepoint[1]>=0: 233 | gaama = m.asin(new_vxaxis[1]) 234 | elif new_referencepoint[1]<=0: 235 | gaama = m.pi - m.asin(new_vxaxis[1]) 236 | else: 237 | gaama = 2*m.pi + m.asin(new_vxaxis[1]) 238 | 239 | X,Y,Z = self.rotate_zaxis((X,Y,Z),theeta = -gaama) 240 | X = X*self.unit_pixels + self.csize/2 241 | Y = self.csize/2 - Y*self.unit_pixels 242 | return_polygon.append((X,Y)) 243 | return_polygon.append('#%02x%02x%02x' % polygon[-1]) 244 | 245 | return_surface.append(return_polygon) 246 | return_coords.append(return_surface) 247 | 248 | return return_coords 249 | 250 | def delete_polygon(self): 251 | for polygon in self.printed_polygons: 252 | self.canvas.delete(polygon) 253 | 254 | self.printed_polygons = [] 255 | 256 | def print_object(self , during_animation = 0): 257 | self.delete_polygon() 258 | twoD_coords = self.display_surfaces(self.threeD_to_twoD()) 259 | self.dynamic_colors() 260 | for surface in twoD_coords: 261 | for polygon in surface: 262 | self.printed_polygons.append(self.canvas.create_polygon(polygon[:-1], fill = polygon[-1])) 263 | self.canvas.update() 264 | 265 | if during_animation: 266 | t.sleep(1/self.frame_rate) 267 | 268 | def change_angles(self, change_alpha, change_beeta, change_gaama): 269 | self.alpha += change_alpha 270 | self.beeta += change_beeta 271 | self.gaama += change_gaama 272 | self.set_view_point() 273 | 274 | def set_angles(self, alpha = None, beeta = None, gaama = None): 275 | if alpha == None and beeta == None and gaama ==None: 276 | pass 277 | else: 278 | self.alpha = alpha 279 | self.beeta = beeta 280 | self.gaama = gaama 281 | self.set_view_point() 282 | 283 | @staticmethod 284 | def rotate_xaxis(point, theeta = None, cos_val = None, sin_val = None): 285 | if cos_val == None: 286 | cos_val = m.cos(theeta) 287 | if sin_val == None: 288 | sin_val = m.sin(theeta) 289 | 290 | x,y,z = point 291 | Y = y*cos_val - z*sin_val 292 | Z = y*sin_val + z*cos_val 293 | 294 | return (x,Y,Z) 295 | 296 | @staticmethod 297 | def rotate_yaxis(point, theeta = None, cos_val = None, sin_val = None): 298 | if cos_val == None: 299 | cos_val = m.cos(theeta) 300 | if sin_val == None: 301 | sin_val = m.sin(theeta) 302 | 303 | x,y,z = point 304 | X = x*cos_val + z*sin_val 305 | Z = -x*sin_val + z*cos_val 306 | 307 | return (X,y,Z) 308 | 309 | @staticmethod 310 | def rotate_zaxis(point, theeta = None, cos_val = None, sin_val = None): 311 | if cos_val == None: 312 | cos_val = m.cos(theeta) 313 | if sin_val == None: 314 | sin_val = m.sin(theeta) 315 | 316 | x,y,z = point 317 | X = x*cos_val - y*sin_val 318 | Y = x*sin_val + y*cos_val 319 | 320 | return (X,Y,z) 321 | 322 | def rotate_point_about_line(self, point, angle, line_vector): 323 | a,b,c = line_vector 324 | lamda = m.sqrt(b**2+c**2) 325 | v = m.sqrt(a**2+b**2+c**2) 326 | if lamda == 0: 327 | lamdax = 1 328 | c=1 329 | else: 330 | lamdax = lamda 331 | 332 | p = self.rotate_xaxis(point, cos_val = c/lamdax, sin_val = b/lamdax) 333 | p = self.rotate_yaxis(p, cos_val = lamda/v, sin_val = -a/v) 334 | p = self.rotate_zaxis(p, theeta = angle) 335 | p = self.rotate_yaxis(p, cos_val = lamda/v, sin_val = a/v) 336 | p = self.rotate_xaxis(p, cos_val = c/lamdax, sin_val = -b/lamdax) 337 | 338 | return p 339 | 340 | def set_virtual_axis(self, reference_point = (0,1,0)): 341 | self.reference_point = reference_point 342 | x1,y1,z1 = reference_point 343 | x2,y2,z2 = self.a,self.b,self.c 344 | self.vxaxis = (y1*z2-y2*z1, x2*z1-x1*z2, x1*y2-x2*y1) 345 | 346 | def set_first_click(self, event): 347 | self.mouse_loc = (event.x, event.y) 348 | 349 | def change_view_angle(self, event): 350 | self.canvas.unbind('', self.move) 351 | x_diff = event.x - self.mouse_loc[0] 352 | y_diff = event.y - self.mouse_loc[1] 353 | const = m.pi/(self.unit_pixels*4) 354 | alpha_change = -x_diff * const 355 | beeta_change = y_diff * const 356 | 357 | new_viewpoint = self.rotate_point_about_line((self.a,self.b,self.c),alpha_change,self.reference_point) 358 | new_viewpoint = self.rotate_point_about_line(new_viewpoint,-beeta_change,self.vxaxis) 359 | new_referencepoint = self.rotate_point_about_line(self.reference_point,-beeta_change,self.vxaxis) 360 | 361 | self.set_view_point(new_viewpoint,new_referencepoint) 362 | 363 | self.print_object(1) 364 | 365 | self.mouse_loc = (event.x, event.y) 366 | self.move = self.canvas.bind('', self.change_view_angle) 367 | 368 | def dynamic_movement(self): 369 | self.start_move = self.canvas.bind('', self.set_first_click) 370 | self.move = self.canvas.bind('', self.change_view_angle) 371 | 372 | def stop_dynamic_movement(self): 373 | self.canvas.unbind('', self.start_move) 374 | self.canvas.unbind('',self.move) 375 | 376 | def change_color(self, colors): 377 | for i in range(len(self.coords)): 378 | for j in range(len(self.coords[i])): 379 | self.colors[i][j][-1] = colors[i][j] 380 | 381 | def set_color(self, colors = None): 382 | if colors == None: 383 | self.colors = [] 384 | for surface in self.coords: 385 | s = [] 386 | for polygon in surface: 387 | s.append(polygon[-1]) 388 | self.colors.append(s) 389 | else: 390 | self.colors = colors 391 | 392 | def dynamic_colors(self): 393 | 394 | a1,b1,c1 = self.a,self.b,self.c 395 | 396 | for i in range(len(self.coords)): 397 | a2,b2,c2 = self.s_equations[i][0],self.s_equations[i][1],self.s_equations[i][2] 398 | d = self.dist((a2,b2,c2),(0,0,0)) 399 | a2,b2,c2 = a2/d,b2/d,c2/d 400 | 401 | cos_angle = a1*a2+b1*b2+c1*c2 402 | if cos_angle>=0: 403 | for j in range(len(self.coords[i])): 404 | r,g,b = self.colors[i][j] 405 | r,g,b = r*cos_angle + r/3*(1-cos_angle),g*cos_angle + g/3*(1-cos_angle),b*cos_angle + b/3*(1-cos_angle) 406 | self.coords[i][j][-1] = (int(r),int(g),int(b)) 407 | 408 | -------------------------------------------------------------------------------- /examples/3DExample/requirements.txt: -------------------------------------------------------------------------------- 1 | sv-ttk 2 | customtkinter 3 | tknodesystem -------------------------------------------------------------------------------- /examples/3DExample/threeDapp.py: -------------------------------------------------------------------------------- 1 | # Author: Akash Bora 2 | # License: MIT 3 | 4 | import customtkinter 5 | from tknodesystem import * 6 | from ctk_spinbox import CTkSpinbox 7 | from tkinter.colorchooser import askcolor 8 | from render_engine import ThreeDFrame 9 | import sv_ttk 10 | from tkinter import ttk, filedialog 11 | import math 12 | 13 | customtkinter.set_appearance_mode("Dark") 14 | 15 | def hex_to_rgb(value): 16 | value = value.lstrip('#') 17 | lv = len(value) 18 | return tuple(int(value[i:i + lv // 3], 16) for i in range(0, lv, lv // 3)) 19 | 20 | def to_points(node, x): 21 | for i in x: 22 | if len(i)!=3: 23 | node.toggle(1) 24 | if type(i) is list: 25 | x[x.index(i)] = tuple(i) 26 | node.configure(text=f"SIDE \npoints: {len(x)}") 27 | return x, hex_to_rgb(node.node_color) 28 | 29 | def angle(v1, v2): 30 | dot = v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2] 31 | mag1 = math.sqrt(v1[0] ** 2 + v1[1] ** 2 + v1[2] ** 2) 32 | mag2 = math.sqrt(v2[0] ** 2 + v2[1] ** 2 + v2[2] ** 2) 33 | 34 | return math.acos(dot / (mag1 * mag2)) 35 | 36 | def sort_points(points): 37 | # sort angles based on the right hand rule 38 | origin = points[0] 39 | ref = (1, 0, 0) 40 | angles = [] 41 | for point in points[1:]: 42 | vector = (point[0] - origin[0], point[1] - origin[1], point[2] - origin[2]) 43 | theta = angle(vector, ref) 44 | angles.append((point, theta)) 45 | angles.sort(key=lambda x: x[1]) 46 | sorted_points = [origin] 47 | for i in angles: 48 | sorted_points.append(i[0]) 49 | return sorted_points 50 | 51 | def join_coords(values): 52 | 53 | geo_data = [] 54 | for i in values: 55 | geo_data.append(i[0]) 56 | 57 | coords_data = [] 58 | all_points = set() 59 | 60 | for side in geo_data: 61 | for points in side: 62 | all_points.add(points) 63 | 64 | num = 0 65 | 66 | coords_data.append(list(all_points)) 67 | for side in geo_data: 68 | 69 | side = sort_points(side) 70 | anti_side = side[::-1] 71 | 72 | connections = [] 73 | 74 | for i in side: 75 | connections.append(list(all_points).index(i)) 76 | 77 | connections.append(values[num][1]) 78 | 79 | coords_data.append([connections]) 80 | 81 | connections = [] 82 | 83 | for i in anti_side: 84 | connections.append(list(all_points).index(i)) 85 | 86 | connections.append(values[num][1]) 87 | 88 | coords_data.append([connections]) 89 | num +=1 90 | 91 | for i in coords_data: 92 | if i==[]: 93 | return 94 | 95 | try: 96 | geo[notebook.index(notebook.select())+1].configure(coords=coords_data) 97 | except: 98 | pass 99 | 100 | def add_content(tab, tab_num): 101 | global dialog_box 102 | 103 | def open_color_dialog(node): 104 | global global_side_color 105 | color = askcolor(title="Choose Side Color") 106 | if color[1]: 107 | node.configure(fg_color=color[1], text_color="black") 108 | global_side_color = color[1] 109 | node.update() 110 | 111 | def open_config_dialog(node): 112 | global dialog_box 113 | 114 | if dialog_box: 115 | dialog_box.destroy() 116 | 117 | def configure_value_node(): 118 | value = (x.get(), y.get(), z.get()) 119 | node.configure(text=str(value), value=value) 120 | 121 | dialog_box = customtkinter.CTkToplevel(root) 122 | dialog_box.resizable(False, False) 123 | dialog_box.transient(root) 124 | dialog_box.attributes("-alpha", 0.9) 125 | dialog_box.title("Configure XYZ") 126 | 127 | customtkinter.CTkLabel(dialog_box, text="X Coordinate").pack(padx=5, pady=10) 128 | x = CTkSpinbox(dialog_box, from_=-20, to=20, command=configure_value_node) 129 | x.pack(expand=True, fill="x", padx=5) 130 | x.set(node.get()[0]) 131 | 132 | customtkinter.CTkLabel(dialog_box, text="Y Coordinate").pack(padx=5, pady=10) 133 | y = CTkSpinbox(dialog_box, from_=-20, to=20, command=configure_value_node) 134 | y.pack(expand=True, fill="x", padx=5) 135 | y.set(node.get()[1]) 136 | 137 | customtkinter.CTkLabel(dialog_box, text="Z Coordinate").pack(padx=5, pady=10) 138 | z = CTkSpinbox(dialog_box, from_=-20, to=20, command=configure_value_node) 139 | z.pack(expand=True, fill="x", padx=5, pady=(0,10)) 140 | z.set(node.get()[2]) 141 | 142 | spawn_x = int(root.winfo_width() * .5 + root.winfo_x() - .5 * dialog_box.winfo_width() + 7) 143 | spawn_y = int(root.winfo_height() * .5 + root.winfo_y() - .5 * dialog_box.winfo_height() + 20) 144 | dialog_box.geometry(f"+{spawn_x}+{spawn_y}") 145 | 146 | def add_point(): 147 | point_node = NodeValue(canvas, text=f"(0, 0, 0)", value=(0,0,0)) 148 | point_node.bind("", lambda e: open_config_dialog(point_node)) 149 | 150 | def add_side(): 151 | side_node = NodeOperation(canvas, text=f"SIDE", inputs=1, multiple_connection=True, 152 | command=to_points, pass_node_id=True) 153 | side_node.bind("", lambda e: open_color_dialog(side_node)) 154 | if global_side_color: 155 | side_node.configure(fg_color=global_side_color, text_color="black") 156 | 157 | def load_canvas(): 158 | if open_file := filedialog.askopenfilename(filetypes=[ 159 | ("JSON", ["*.json"]), 160 | ("All Files", "*.*")]): 161 | canvas.load(open_file) 162 | 163 | for i in canvas.node_list: 164 | if type(i) is NodeOperation: 165 | i.bind("", lambda e, n=i: open_color_dialog(n)) 166 | elif type(i) is NodeValue: 167 | i.bind("", lambda e, n=i: open_config_dialog(n)) 168 | 169 | def save_canvas(): 170 | save_file = filedialog.asksaveasfilename(defaultextension=".json", 171 | filetypes=[('json', ['*.json']),('All Files', '*.*')]) 172 | if save_file: 173 | canvas.save(save_file) 174 | 175 | frame_left = customtkinter.CTkFrame(tab) 176 | frame_left.pack(expand=True, fill="both", padx=10, pady=10, side="left") 177 | 178 | frame_right = customtkinter.CTkFrame(tab, width=500) 179 | frame_right.pack(fill="y", padx=(0,10), pady=10, side="right") 180 | 181 | geo[tab_num] = ThreeDFrame(frame_right, bg=frame_right.cget("fg_color")[1], 182 | coords=default_pyramid) 183 | geo[tab_num].pack(fill="both", expand=True) 184 | 185 | label = customtkinter.CTkLabel(frame_right, text="3D Viewer") 186 | label.pack(fill="x", pady=0) 187 | 188 | scale = customtkinter.CTkSlider(frame_right, 189 | from_=0, 190 | to=100, 191 | command=lambda e: geo[tab_num].configure(size=int(e))) 192 | scale.pack(padx=5, fill="x") 193 | scale.set(100) 194 | 195 | open_button = customtkinter.CTkButton(frame_right, text="OPEN", width=200, command=load_canvas) 196 | open_button.pack(fill="x", padx=(10,5), pady=10, side="left") 197 | 198 | save_button = customtkinter.CTkButton(frame_right, text="SAVE", width=200, command=save_canvas) 199 | save_button.pack(fill="x", padx=(5,10), pady=10, side="left") 200 | 201 | canvas = NodeCanvas(frame_left, bg=frame_left._fg_color[1], width=800, height=500) 202 | canvas.pack(expand=True, fill="both", padx=5, pady=5) 203 | 204 | comp = NodeCompile(canvas, fixed=True, text="Render", 205 | multiple_connection=True, show_value=False, 206 | command=join_coords) 207 | 208 | canvas.rowconfigure(0, weight=1) 209 | 210 | button_1 = customtkinter.CTkButton(canvas, text="Add Point", width=80, command=add_point) 211 | button_1.grid(pady=10, padx=10, sticky="se") 212 | 213 | button_2 = customtkinter.CTkButton(canvas, text="Add Side", width=80, command=add_side) 214 | button_2.grid(pady=10, padx=10, sticky="se") 215 | 216 | def add_tab(): 217 | global tab_num 218 | if tab_num>9: 219 | add_tab_button.configure(state="disabled") 220 | return 221 | tab_num +=1 222 | tab = ttk.Frame(notebook, takefocus=0) 223 | notebook.add(tab, text=f"Tab {tab_num}") 224 | add_content(tab, tab_num) 225 | 226 | tab_num = 1 227 | root = customtkinter.CTk() 228 | root.title("3D Geometry Viewer") 229 | root.geometry("1200x550") 230 | customtkinter.deactivate_automatic_dpi_awareness() 231 | root.resizable(False, False) 232 | 233 | geo = {} 234 | dialog_box = None 235 | global_side_color = "#ffffff" 236 | notebook = ttk.Notebook(root, takefocus=False) 237 | notebook.pack(expand=True, fill="both") 238 | 239 | default_pyramid = [[(0, 1, 0), (-1, -1, -1), (1, -1, 1), (-1, -1, 1), (1, -1, -1)], 240 | [[2, 4, 1, 3, (255, 255, 255)]], [[3, 1, 4, 2, (255, 255, 255)]], 241 | [[2, 0, 3, (255, 255, 255)]], [[3, 0, 2, (255, 255, 255)]], 242 | [[2, 4, 0, (255, 255, 255)]], [[0, 4, 2, (255, 255, 255)]], 243 | [[1, 4, 0, (255, 255, 255)]], [[0, 4, 1, (255, 255, 255)]], 244 | [[3, 0, 1, (255, 255, 255)]], [[1, 0, 3, (255, 255, 255)]]] 245 | 246 | tab_1 = ttk.Frame(notebook, takefocus=0) 247 | notebook.add(tab_1, text=f"Tab 1") 248 | 249 | add_content(tab_1, 1) 250 | 251 | style = ttk.Style() 252 | 253 | style.layout("Tab", [('Notebook.tab', {'sticky': 'nswe', 'children': 254 | [('Notebook.padding', {'side': 'top', 'sticky': 'nswe', 'children': 255 | [('Notebook.label', {'side': 'top', 'sticky': ''})], 256 | })], 257 | })] 258 | ) 259 | 260 | add_tab_button = customtkinter.CTkButton(notebook, width=30, text="+", 261 | bg_color="#2f2f2f", command=add_tab) 262 | add_tab_button.pack(anchor="ne", pady=5, padx=10) 263 | sv_ttk.set_theme("dark") 264 | root.mainloop() 265 | -------------------------------------------------------------------------------- /examples/advanced_example.py: -------------------------------------------------------------------------------- 1 | # Advanced Example of TkNodeSystem 2 | # Author: Akash Bora 3 | # License: MIT 4 | 5 | from PIL import ImageEnhance, Image # pip install pillow 6 | import customtkinter 7 | from tknodesystem import * 8 | import tkinter 9 | 10 | root = customtkinter.CTk() 11 | root.title("Image Enhancement") 12 | 13 | frame_left = customtkinter.CTkFrame(root) 14 | frame_left.pack(expand=True, fill="both", padx=10, pady=10, side="left") 15 | 16 | frame_right = customtkinter.CTkFrame(root) 17 | frame_right.pack(expand=True, fill="both", padx=10, pady=10, side="right") 18 | 19 | image_label = customtkinter.CTkLabel(frame_right, corner_radius=20, width=600, height=400, text="") 20 | image_label.pack(expand=True, fill="both", padx=5, pady=10) 21 | 22 | label = customtkinter.CTkLabel(frame_right, text="Image Viewer") 23 | label.pack(expand=True, fill="both", padx=5, pady=0) 24 | 25 | canvas = NodeCanvas(frame_left, bg=frame_left._fg_color[1], width=800, height=500) 26 | canvas.pack(expand=True, fill="both", padx=5, pady=5) 27 | 28 | sliders = {} 29 | x = 1 30 | img = None 31 | 32 | def show_current_slider(node, num): 33 | """ command when a node is clicked, places only the required slider """ 34 | for i in sliders.values(): 35 | i.pack_forget() 36 | 37 | sliders[num].pack(expand=True, fill="x", padx=20, pady=5, side="bottom") 38 | label.configure(text=node.text) 39 | 40 | def add_slider(num, node): 41 | """ add some sliders when a new node is placed """ 42 | def update(e): 43 | node.update() 44 | label.configure(text=f"{node.text}: {round(e,2)}") 45 | 46 | for i in sliders.values(): 47 | i.pack_forget() 48 | 49 | sliders[num] = customtkinter.CTkSlider(frame_right, from_=0, to=10, command=lambda e: update(e)) 50 | sliders[num].pack(expand=True, fill="x", padx=20, pady=5, side="bottom") 51 | sliders[num].set(1) 52 | 53 | def input_media(): 54 | """ input node which contains a file data """ 55 | file = tkinter.filedialog.askopenfilename(filetypes =[('Images', ['*.png','*.jpg','*.jpeg','*.bmp','*webp']),('All Files', '*.*')]) 56 | if file: 57 | NodeValue(canvas, value=Image.open(file), text="MediaIn") 58 | 59 | def add_effect(effect): 60 | """ node operations """ 61 | global x 62 | def image_modify(img, value): 63 | try: 64 | if effect=="Brightness": 65 | mode = ImageEnhance.Brightness(img) 66 | elif effect=="Contrast": 67 | mode = ImageEnhance.Contrast(img) 68 | elif effect=="Sharpness": 69 | mode = ImageEnhance.Sharpness(img) 70 | elif effect=="Color": 71 | mode = ImageEnhance.Color(img) 72 | 73 | return mode.enhance(sliders[value].get()) 74 | except AttributeError: None 75 | 76 | value = x 77 | node = NodeOperation(canvas, inputs=1, text=effect, command=lambda img: image_modify(img, value), 78 | click_command=lambda: show_current_slider(node, value)) 79 | label.configure(text=node.text) 80 | add_slider(value, node) 81 | x += 1 82 | 83 | def show_image(output): 84 | """ compile operation: shows the output image """ 85 | 86 | global img 87 | label_height = frame_right.winfo_height() 88 | ratio = output.size[1]/output.size[0] 89 | if ratio>1.5: ratio = 1.5 90 | if ratio<0.5: ratio = 0.5 91 | img = customtkinter.CTkImage(output, size=(label_height, label_height*ratio)) 92 | image_label.configure(image=img) 93 | 94 | menu = NodeMenu(canvas) # right click or press to spawn the node menu 95 | menu.add_node(label="Media Import", command=input_media) 96 | menu.add_node(label="Media Out", command=lambda: NodeCompile(canvas, text="MediaOut", show_value=None, command=show_image)) 97 | menu.add_node(label="Brightness", command=lambda: add_effect("Brightness")) 98 | menu.add_node(label="Contrast", command=lambda: add_effect("Contrast")) 99 | menu.add_node(label="Color", command=lambda: add_effect("Color")) 100 | menu.add_node(label="Sharpness", command=lambda: add_effect("Sharpness")) 101 | 102 | root.mainloop() 103 | -------------------------------------------------------------------------------- /examples/maths_with_tknodes.py: -------------------------------------------------------------------------------- 1 | # Example of TkNodeSystem 2 | # Author: Akash Bora 3 | # License: MIT 4 | 5 | import customtkinter 6 | from tknodesystem import * 7 | 8 | root = customtkinter.CTk() 9 | root.geometry("800x500") 10 | root.title("Maths with TkNodes") 11 | 12 | canvas = NodeCanvas(root) 13 | canvas.pack(fill="both", expand=True) 14 | 15 | canvas.rowconfigure(0, weight=1) 16 | button_1 = customtkinter.CTkButton(canvas, text="save", width=50, command=lambda: canvas.save("canvas.json")) 17 | button_1.grid(pady=10, padx=10, sticky="se") 18 | 19 | button_2 = customtkinter.CTkButton(canvas, text="load", width=50, command=lambda: canvas.load("canvas.json")) 20 | button_2.grid(pady=10, padx=10, sticky="se") 21 | 22 | def add_value(): 23 | dialog = customtkinter.CTkInputDialog(text="Type in a number:", title="Value Node") 24 | text = dialog.get_input() 25 | if text is not None: 26 | if text.isdigit(): 27 | NodeValue(canvas, text=f"Value {text}", value=int(text)) 28 | 29 | def add(x,y): 30 | return x+y 31 | 32 | def power(x,y): 33 | return x**y 34 | 35 | def sub(x,y): 36 | return x-y 37 | 38 | def div(x,y): 39 | if y!=0: 40 | return x/y 41 | else: 42 | return None 43 | 44 | def mul(x,y): 45 | return x*y 46 | 47 | def mod(x): 48 | return abs(x) 49 | 50 | menu = NodeMenu(canvas) # right click or press to spawn the node menu 51 | menu.add_node(label="Value", command=add_value) 52 | menu.add_node(label="Output", command=lambda: NodeCompile(canvas)) 53 | menu.add_node(label="Add/Sum", command=lambda: NodeOperation(canvas, text="Add", command=add)) 54 | menu.add_node(label="Subtract", command=lambda: NodeOperation(canvas, text="Sub", command=sub)) 55 | menu.add_node(label="Divide", command=lambda: NodeOperation(canvas, text="Div", command=div)) 56 | menu.add_node(label="Mod", command=lambda: NodeOperation(canvas, inputs=1, text="Mod", command=mod)) 57 | menu.add_node(label="Multiply", command=lambda: NodeOperation(canvas, text="Mul", command=mul)) 58 | menu.add_node(label="Power", command=lambda: NodeOperation(canvas, text="Power", command=power)) 59 | root.mainloop() 60 | -------------------------------------------------------------------------------- /examples/simple_example.py: -------------------------------------------------------------------------------- 1 | from tknodesystem import * 2 | import tkinter 3 | 4 | root = tkinter.Tk() 5 | 6 | # Node Canvas 7 | canvas = NodeCanvas(root) 8 | canvas.pack(fill="both", expand=True) 9 | 10 | # Node Types 11 | NodeValue(canvas, x=0, y=10) 12 | NodeOperation(canvas, x=150, y=1) 13 | NodeCompile(canvas, x=300, y=10) 14 | 15 | # Node Menu 16 | menu = NodeMenu(canvas) # right click or press to spawn the node menu 17 | menu.add_node(label="NodeValue", command=lambda: NodeValue(canvas)) 18 | menu.add_node(label="NodeOperation", command=lambda: NodeOperation(canvas)) 19 | menu.add_node(label="NodeCompile", command=lambda: NodeCompile(canvas)) 20 | 21 | root.mainloop() 22 | -------------------------------------------------------------------------------- /tknodesystem/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Node System in Tkinter 3 | Author: Akash Bora 4 | Homepage: https://github.com/Akascape/TkNodeSystem 5 | License: MIT 6 | """ 7 | 8 | __version__ = '0.9' 9 | 10 | from .node_types import NodeValue, NodeOperation, NodeCompile 11 | from .node_canvas import NodeCanvas 12 | from .node_menu import NodeMenu 13 | -------------------------------------------------------------------------------- /tknodesystem/grid_images/grid_dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akascape/TkNodeSystem/a9e9c587c47facda46bd4c06bfc3b2db1fe74606/tknodesystem/grid_images/grid_dot.png -------------------------------------------------------------------------------- /tknodesystem/grid_images/grid_lines.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akascape/TkNodeSystem/a9e9c587c47facda46bd4c06bfc3b2db1fe74606/tknodesystem/grid_images/grid_lines.png -------------------------------------------------------------------------------- /tknodesystem/grid_images/no_grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akascape/TkNodeSystem/a9e9c587c47facda46bd4c06bfc3b2db1fe74606/tknodesystem/grid_images/no_grid.png -------------------------------------------------------------------------------- /tknodesystem/node.py: -------------------------------------------------------------------------------- 1 | class Node: 2 | def __init__(self, canvas, width=50, height=50, border_color='white', border_width=0, justify="center", 3 | fg_color='#37373D', center=(100,50), text='', text_color='white', corner_radius=25, 4 | font=("",10), click_command=None, highlightcolor='#52d66c', hover=True): 5 | 6 | self.canvas = canvas 7 | self.width = width 8 | self.height = height 9 | self.node_outline_color = border_color 10 | self.node_outline_thickness = border_width 11 | self.node_color = fg_color 12 | self.text_color = text_color 13 | self.corner_radius = corner_radius 14 | self.font = font 15 | self.justify = justify 16 | self.text = text 17 | self.center = center 18 | self.click_command = click_command 19 | self.auxlist = [] 20 | self.signal = False 21 | self.hover = hover 22 | self.hover_color = highlightcolor 23 | self.create() 24 | self.canvas.node_list.add(self) 25 | 26 | def create(self): 27 | """ create round rectangular frame with text """ 28 | 29 | self.ID = self.create_round_rectangle(self.center[0]-self.width*0.5, self.center[1]-self.height*0.5, 30 | self.center[0]+self.width*0.5, self.center[1]+self.height*0.5, 31 | radius=self.corner_radius, outline=self.node_outline_color, fill=self.node_color, 32 | width=self.node_outline_thickness) 33 | 34 | self.IDtext = self.canvas.create_text(self.center, fill=self.text_color, justify=self.justify, font=self.font, text=self.text) 35 | self.allIDs = [self.ID, self.IDtext] 36 | self.auxlist = [self.ID, self.IDtext] 37 | 38 | for i in self.auxlist: 39 | self.canvas.tag_bind(i, "", self.getpos, add="+") 40 | self.canvas.tag_bind(i, '', self.enter_node) 41 | self.canvas.tag_bind(i, '', self.leave_node) 42 | 43 | def create_round_rectangle(self, x1, y1, x2, y2, radius=25, **kwargs): 44 | points = [x1+radius, y1, x1+radius, y1, x2-radius, y1, x2-radius, y1, x2, y1, x2, 45 | y1+radius, x2, y1+radius, x2, y2-radius, x2, y2-radius, x2, y2, 46 | x2-radius, y2, x2-radius, y2, x1+radius, y2, x1+radius, y2, x1, y2, x1, 47 | y2-radius, x1, y2-radius, x1, y1+radius, x1, y1+radius, x1, y1] 48 | 49 | return self.canvas.create_polygon(points, **kwargs, smooth=True) 50 | 51 | def getpos(self, event): 52 | """ bind click command and raise the node""" 53 | self.xy_set = (event.x, event.y) 54 | 55 | if self.click_command: self.click_command() 56 | 57 | for id_ in self.allIDs: 58 | self.canvas.tag_raise(id_) 59 | 60 | def bind_all_to_movement(self): 61 | """ bind the node items to motion """ 62 | 63 | for id_ in self.auxlist: 64 | self.canvas.tag_bind(id_, '', self.mouse_mov) 65 | 66 | def mouse_mov(self, event): 67 | """ place the node items based one the motion of mouse """ 68 | 69 | for id_ in self.allIDs: 70 | self.canvas.move(id_, event.x-self.xy_set[0], event.y-self.xy_set[1]) 71 | self.canvas.tag_raise(id_) 72 | 73 | self.xy_set = (event.x, event.y) 74 | 75 | self.update_sockets() 76 | 77 | def update_sockets(self): 78 | """ update the coordinates of sockets and lines """ 79 | self.output_.update() 80 | try: 81 | self.input_1.update() 82 | self.input_2.update() 83 | self.input_3.update() 84 | self.input_4.update() 85 | self.input_5.update() 86 | except AttributeError: None 87 | 88 | for i in self.canvas.line_ids: 89 | i.update() 90 | 91 | deleted = set() 92 | for i in self.canvas.line_ids: 93 | if not i.connected: 94 | deleted.add(i) 95 | self.canvas.line_ids = self.canvas.line_ids.difference(deleted) 96 | 97 | def move(self, x, y): 98 | for id_ in self.allIDs: 99 | self.canvas.move(id_, x, y) 100 | self.canvas.tag_raise(id_) 101 | self.update_sockets() 102 | 103 | def enter_node(self, event): 104 | if self.hover: 105 | self.canvas.itemconfigure(self.ID, outline=self.hover_color, width=self.node_outline_thickness+1) 106 | self.signal = True 107 | 108 | def leave_node(self, event): 109 | if self.hover: 110 | self.canvas.itemconfigure(self.ID, outline=self.node_outline_color, width=self.node_outline_thickness) 111 | self.signal = False 112 | 113 | def destroy(self): 114 | self.canvas.delete(self.ID, self.IDtext) 115 | self.canvas.node_list.remove(self) 116 | for i in self.canvas.line_ids: 117 | i.update() 118 | 119 | def bind(self, binding, command, add="+"): 120 | for i in self.auxlist: 121 | self.canvas.tag_bind(i, binding, command, add) 122 | 123 | def configure(self, **kwargs): 124 | """ configure options """ 125 | 126 | if "fg_color" in kwargs: 127 | self.node_color = kwargs.pop("fg_color") 128 | self.canvas.itemconfig(self.ID, fill=self.node_color) 129 | if "highlightcolor" in kwargs: 130 | self.hover_color = kwargs.pop("highlightcolor") 131 | if "hover" in kwargs: 132 | self.hover = kwargs.pop("hover") 133 | if "text" in kwargs: 134 | self.text = kwargs.pop("text") 135 | self.canvas.itemconfig(self.IDtext, text=self.text) 136 | if "text_color" in kwargs: 137 | self.text_color = kwargs.pop("text_color") 138 | self.canvas.itemconfig(self.IDtext, fill=self.text_color) 139 | if "font" in kwargs: 140 | self.font = kwargs.pop("font") 141 | self.canvas.itemconfig(self.IDtext, font=self.font) 142 | if len(kwargs)>0: 143 | raise ValueError("This option is not configurable:" + list(kwargs.keys())[0]) 144 | -------------------------------------------------------------------------------- /tknodesystem/node_args.py: -------------------------------------------------------------------------------- 1 | class Args(): 2 | """ This class is used to remove default arguments in order to reduce the size of the export file """ 3 | 4 | def value_args(args): 5 | default_args = {'width': 100, 'height': 50, 'value': 0, 'border_color': 'white', 'text': None, 6 | 'corner_radius': 25, 'border_width': 0, 'fg_color': '#37373D', 'text_color': 'white', 7 | 'font': ('', 10), 'socket_radius': 8, 'socket_hover': True, 'socket_color': 'green', 8 | 'socket_hover_color': 'grey50', 'highlightcolor': '#52d66c', 'hover': True, 'fixed': False, 9 | 'click_command': None, 'side': 'right', 'x': None, 'y': None, 'num': None, 'justify': 'center'} 10 | 11 | args.pop("canvas") 12 | args.pop("self") 13 | args.pop("__class__") 14 | args.pop("x") 15 | args.pop("y") 16 | args.pop("click_command") 17 | args.pop("num") 18 | new_args = {} 19 | 20 | for i in args.keys(): 21 | if default_args.get(i)!=args.get(i): 22 | new_args.update({i:args.get(i)}) 23 | 24 | return new_args 25 | 26 | def func_args(args): 27 | default_args = {'width': 100, 'height': 80, 'inputs': 2, 'border_color': '#37373D', 'text': None, 'socket_radius': 8, 28 | 'corner_radius': 25, 'border_width': 0, 'fg_color': '#37373D', 'text_color': 'white', 'font': ('', 10), 29 | 'highlightcolor': '#52d66c', 'hover': True, 'socket_color': 'green', 'socket_hover_color': 'grey50', 'pass_node_id': False, 30 | 'x': None, 'y': None, 'multiside': False, 'output_socket_color': 'green', 'click_command': None, 'fixed': False, 31 | 'socket_hover': True, 'num': None, 'none_inputs': False, 'justify': 'center', 'hover_text': None, 'multiple_connection': False} 32 | 33 | args.pop("canvas") 34 | args.pop("self") 35 | args.pop("__class__") 36 | args.pop("command") 37 | args.pop("x") 38 | args.pop("y") 39 | args.pop("click_command") 40 | args.pop("num") 41 | new_args = {} 42 | 43 | for i in args.keys(): 44 | if default_args.get(i)!=args.get(i): 45 | new_args.update({i:args.get(i)}) 46 | 47 | return new_args 48 | 49 | def compile_args(args): 50 | default_args = {'width': 100, 'height': 50, 'border_color': '#37373D', 'text': 'Compile', 'socket_radius': 8, 'justify': 'center', 'pass_node_id': False, 51 | 'corner_radius': 25, 'x': None, 'y': None, 'border_width': 0, 'fg_color': '#37373D', 'text_color': 'white', 'multiple_connection': False, 52 | 'font': ('', 10), 'highlightcolor': '#52d66c', 'hover': True, 'socket_hover': True, 'socket_color': 'green', 'fixed': False, 53 | 'socket_hover_color': 'grey50', 'show_value': True, 'command': None, 'click_command': None, 'side': 'left', 'num': None} 54 | 55 | args.pop("canvas") 56 | args.pop("self") 57 | args.pop("__class__") 58 | args.pop("command") 59 | args.pop("x") 60 | args.pop("y") 61 | args.pop("click_command") 62 | args.pop("num") 63 | new_args = {} 64 | 65 | for i in args.keys(): 66 | if default_args.get(i)!=args.get(i): 67 | new_args.update({i:args.get(i)}) 68 | 69 | return new_args 70 | -------------------------------------------------------------------------------- /tknodesystem/node_canvas.py: -------------------------------------------------------------------------------- 1 | import tkinter 2 | import platform 3 | import os 4 | import json 5 | import sys 6 | from .node_wire import NodeWire 7 | from .node_types import NodeValue, NodeOperation, NodeCompile 8 | 9 | class NodeCanvas(tkinter.Canvas): 10 | def __init__(self, master, bg="grey10", width=500, height=500, wire_color="white", wire_width=3, 11 | grid_image="lines", zoom=True, wire_dash=True, move=True, wire_hover_color="red", **kwargs): 12 | 13 | super().__init__(master, bg=bg, width=width, height=height, bd=0, highlightthickness=0, **kwargs) 14 | 15 | self.wire_color = wire_color 16 | self.wire_width = wire_width 17 | self.wire_hover_color = wire_hover_color 18 | self.dash = wire_dash 19 | self.bg = bg 20 | self.inputcell = None 21 | self.outputcell = None 22 | self.clickcount = 0 23 | self.IDc = None 24 | self.operation_num = 0 25 | self.input_num = 0 26 | self.compile_num = 0 27 | self.socket_num = 0 28 | self.connect_wire = True 29 | self.line_list = set() 30 | self.obj_list = set() 31 | self.line_ids = set() 32 | self.node_list = set() 33 | self.gain_in = 1 34 | self.gain_out = 1 35 | self.set_grid_image(grid_image) 36 | 37 | if move: 38 | if sys.platform.startswith("darwin"): 39 | self.tag_bind(self.grid_bg, '', lambda e: self.getpos(e, 1)) 40 | self.tag_bind(self.grid_bg, '', lambda e: self.getpos(e, 0)) 41 | self.tag_bind(self.grid_bg, "", self.move_grid) 42 | else: 43 | self.tag_bind(self.grid_bg, '', lambda e: self.getpos(e, 1)) 44 | self.tag_bind(self.grid_bg, '', lambda e: self.getpos(e, 0)) 45 | self.tag_bind(self.grid_bg, "", self.move_grid) 46 | 47 | if zoom: 48 | self.bind("", self.do_zoom) 49 | self.tag_bind(self.grid_bg, "", lambda e: self.do_zoom(e, 120)) 50 | self.tag_bind(self.grid_bg, "", lambda e: self.do_zoom(e, -120)) 51 | 52 | def set_grid_image(self, grid_image): 53 | """ set the grid image for the canvas """ 54 | 55 | # default grids: ['dots', 'lines', None] 56 | base_path = os.path.dirname(os.path.abspath(__file__)) 57 | if grid_image=="dots": 58 | self.image = tkinter.PhotoImage(file=os.path.join(base_path, "grid_images", "grid_dot.png")) 59 | elif grid_image=="lines": 60 | self.image = tkinter.PhotoImage(file=os.path.join(base_path, "grid_images", "grid_lines.png")) 61 | elif not grid_image: 62 | self.image = tkinter.PhotoImage(file=os.path.join(base_path, "grid_images", "no_grid.png")) 63 | else: 64 | self.image = tkinter.PhotoImage(file=grid_image) 65 | 66 | self.image = self.image.subsample(1,1) 67 | try: 68 | self.delete(self.grid_bg) 69 | except: None 70 | self.grid_bg = self.create_image(self.image.width()/2, self.image.height()/2, image = self.image) 71 | self.tag_lower(self.grid_bg) 72 | 73 | def getpos(self, event, cursor): 74 | """ get the mouse position and change cursor style """ 75 | 76 | self.xy_set = (event.x, event.y) 77 | 78 | if cursor: 79 | self.config(cursor="fleur", width=self.winfo_reqwidth(), height=self.winfo_reqheight()) 80 | else: 81 | self.config(cursor="arrow", width=self.winfo_reqwidth(), height=self.winfo_reqheight()) 82 | 83 | def move_grid(self, event): 84 | """ move the contents of the canvas except the grid image """ 85 | 86 | self.all_items = list(self.find_all()) 87 | self.all_items.pop(self.all_items.index(self.grid_bg)) 88 | 89 | for i in self.all_items: 90 | self.move(i, event.x-self.xy_set[0], event.y-self.xy_set[1]) 91 | self.xy_set = (event.x, event.y) 92 | 93 | for i in self.node_list: 94 | i.update_sockets() 95 | 96 | def do_zoom(self, event, delta=None): 97 | """ zoom in/out the canvas by changing the coordinates of all canvas items """ 98 | 99 | self.all_items = list(self.find_all()) 100 | self.all_items.pop(self.all_items.index(self.grid_bg)) 101 | 102 | if not delta: 103 | delta = event.delta 104 | 105 | if delta>0: 106 | for i in self.all_items: 107 | self.scale(i, event.x, event.y, 1.1, 1.1) 108 | self.gain_in +=1 109 | else: 110 | for i in self.all_items: 111 | self.scale(i, event.x, event.y, 0.9, 0.9) 112 | self.gain_out +=1 113 | 114 | for i in self.node_list: 115 | i.update_sockets() 116 | 117 | def conectcells(self): 118 | """ connection data for the inputs of any operation node """ 119 | 120 | if self.IDc == 'input1': self.inputcell.cellinput1 = self.outputcell 121 | if self.IDc == 'input2': self.inputcell.cellinput2 = self.outputcell 122 | if self.IDc == 'input3': self.inputcell.cellinput3 = self.outputcell 123 | if self.IDc == 'input4': self.inputcell.cellinput4 = self.outputcell 124 | if self.IDc == 'input5': self.inputcell.cellinput5 = self.outputcell 125 | 126 | if self.inputcell is None or self.outputcell is None: 127 | return 128 | if self.inputcell.ID!=self.outputcell.ID: 129 | self.line = NodeWire(self, self.outputcell, self.inputcell, wire_color=self.wire_color, 130 | wire_width=self.wire_width, dash=self.dash, wire_hover_color=self.wire_hover_color) 131 | self.inputcell.update() 132 | 133 | def clear(self): 134 | """ clear the canvas except the grid image """ 135 | 136 | self.all_items = list(self.find_all()) 137 | self.all_items.pop(self.all_items.index(self.grid_bg)) 138 | 139 | for i in self.all_items: 140 | self.delete(i) 141 | 142 | self.node_num = 0 143 | self.input_num = 0 144 | self.compile_num = 0 145 | self.socket_num = 0 146 | self.line_list = set() 147 | self.obj_list = set() 148 | self.line_ids = set() 149 | self.node_list = set() 150 | 151 | def configure(self, **kwargs): 152 | """ configure options """ 153 | 154 | if "wire_color" in kwargs: 155 | self.wire_color = kwargs.pop("wire_color") 156 | for i in self.line_ids: 157 | i.configure(wire_color=self.wire_color) 158 | if "wire_width" in kwargs: 159 | self.wire_width = kwargs.pop("wire_width") 160 | for i in self.line_ids: 161 | i.configure(wire_width=self.wire_width) 162 | if "wire_dash" in kwargs: 163 | self.dash = kwargs.pop("wire_dash") 164 | for i in self.line_ids: 165 | i.configure(dash=self.dash) 166 | if "wire_hover_color" in kwargs: 167 | self.wire_hover_color = kwargs.pop("wire_hover_color") 168 | for i in self.line_ids: 169 | i.configure(wire_hover_color=self.wire_hover_color) 170 | if "grid_image" in kwargs: 171 | self.set_grid_image(kwargs.pop("grid_image")) 172 | 173 | super().config(**kwargs) 174 | 175 | def save(self, filename): 176 | """ save the node tree to a file """ 177 | if os.path.exists(filename): 178 | os.remove(filename) 179 | 180 | x = [] 181 | sorted_obj_list = [] 182 | for i in self.obj_list: 183 | x.append(i.ID) 184 | 185 | for i in sorted(x): 186 | for x in self.obj_list: 187 | if i==x.ID: 188 | sorted_obj_list.append(x) 189 | 190 | with open(filename, 'w') as file: 191 | obj_dict = {f'{obj.type} {id}': (obj.args, round(obj.output_.center[0]-obj.width,2), 192 | round(obj.output_.center[1]-obj.height,2), obj.socket_nums) for id, obj in enumerate(sorted_obj_list)} 193 | obj_dict.update({"Lines" : list(self.line_list)}) 194 | json.dump(obj_dict, file) 195 | 196 | def load(self, filename): 197 | """ load the node tree back to the canvas """ 198 | 199 | if not os.path.exists(filename): 200 | raise FileNotFoundError("No such file found: " + str(filename)) 201 | 202 | self.clear() 203 | self.connect_wire = False 204 | 205 | obj_type_dict = {'NodeValue': NodeValue, 206 | 'NodeOperation': NodeOperation, 207 | 'NodeCompile': NodeCompile} 208 | 209 | with open(filename) as file: 210 | obj_dict = json.load(file) 211 | value_nodes = [] 212 | func_nodes = [] 213 | comp_nodes = [] 214 | self.obj_list = set() 215 | 216 | for obj_type, attributes in obj_dict.items(): 217 | if obj_type.split()[0]=="Lines": 218 | line_list = attributes 219 | continue 220 | 221 | obj = obj_type_dict[obj_type.split()[0]](self, **attributes[0], x=attributes[1], y=attributes[2], num=attributes[3]) 222 | if obj_type.split()[0]=="NodeValue": 223 | value_nodes.append(obj) 224 | elif obj_type.split()[0]=="NodeOperation": 225 | func_nodes.append(obj) 226 | elif obj_type.split()[0]=="NodeCompile": 227 | comp_nodes.append(obj) 228 | self.obj_list.add(obj) 229 | 230 | for nodes in [value_nodes, func_nodes, comp_nodes]: 231 | for i in nodes: 232 | i.connect_output(None) 233 | for j in func_nodes: 234 | try: 235 | if [self.outputcell.output_.socket_num, j.input_1.socket_num] in line_list: 236 | self.clickcount = 1 237 | j.connect_input(j.line1, 'input1') 238 | if [self.outputcell.output_.socket_num, j.input_2.socket_num] in line_list: 239 | self.clickcount = 1 240 | j.connect_input(j.line2, 'input2') 241 | if [self.outputcell.output_.socket_num, j.input_3.socket_num] in line_list: 242 | self.clickcount = 1 243 | j.connect_input(j.line3, 'input3') 244 | if [self.outputcell.output_.socket_num, j.input_4.socket_num] in line_list: 245 | self.clickcount = 1 246 | j.connect_input(j.line4, 'input4') 247 | if [self.outputcell.output_.socket_num, j.input_5.socket_num] in line_list: 248 | self.clickcount = 1 249 | j.connect_input(j.line5, 'input5') 250 | except AttributeError: None 251 | for j in comp_nodes: 252 | if [self.outputcell.output_.socket_num, j.input_1.socket_num] in line_list: 253 | self.clickcount = 1 254 | j.connect_input(None) 255 | 256 | 257 | self.connect_wire = True 258 | 259 | -------------------------------------------------------------------------------- /tknodesystem/node_menu.py: -------------------------------------------------------------------------------- 1 | import customtkinter 2 | import sys 3 | import time 4 | 5 | customtkinter.set_appearance_mode("Dark") 6 | 7 | class NodeMenu(customtkinter.CTkToplevel): 8 | 9 | def __init__(self, attach, button_color=None, height: int = 300, width: int = 250, text_color=None, 10 | fg_color=None, button_height: int = 30, justify="center", scrollbar_button_color=None, 11 | scrollbar=True, scrollbar_button_hover_color=None, frame_border_width=2, 12 | command=None, alpha: float = 0.97, frame_corner_radius=20, label="Search Nodes", 13 | resize=True, border_color=None, **kwargs): 14 | 15 | super().__init__(takefocus=1) 16 | 17 | self.focus() 18 | self.alpha = alpha 19 | self.attributes('-alpha', 0) 20 | self.corner = frame_corner_radius 21 | 22 | self.attach = attach 23 | self.height = height 24 | self.width = width 25 | self.command = command 26 | self.fade = False 27 | self.resize = resize 28 | self.button_num = 0 29 | self.node = {} 30 | self.padding = 0 31 | self.focus_something = False 32 | 33 | self.withdraw() 34 | if sys.platform.startswith("win"): 35 | self.overrideredirect(True) 36 | self.transparent_color = self._apply_appearance_mode(self._fg_color) 37 | self.attributes("-transparentcolor", self.transparent_color) 38 | self.attach.bind("", self.popup, add="+") 39 | self.bind("", lambda e: self._withdraw()) 40 | elif sys.platform.startswith("darwin"): 41 | self.overrideredirect(True) 42 | self.transparent_color = 'systemTransparent' 43 | self.attributes("-transparent", True) 44 | self.transient(self.master) 45 | self.attach.bind("", lambda e: self._withdraw(), add="+") 46 | self.attach.bind("", self.popup, add="+") 47 | self.focus_something = True 48 | else: 49 | self.attributes("-type", "splash") 50 | self.transparent_color = '#000001' 51 | self.corner = 0 52 | self.padding = 20 53 | self.attach.bind("", self.popup, add="+") 54 | self.bind("", lambda e: self._withdraw()) 55 | 56 | self.fg_color = customtkinter.ThemeManager.theme["CTkFrame"]["fg_color"] if fg_color is None else fg_color 57 | self.scroll_button_color = customtkinter.ThemeManager.theme["CTkScrollbar"]["button_color"] if scrollbar_button_color is None else scrollbar_button_color 58 | self.scroll_hover_color = customtkinter.ThemeManager.theme["CTkScrollbar"]["button_hover_color"] if scrollbar_button_hover_color is None else scrollbar_button_hover_color 59 | self.border_color = customtkinter.ThemeManager.theme["CTkFrame"]["border_color"] if border_color is None else border_color 60 | self.button_color = customtkinter.ThemeManager.theme["CTkFrame"]["top_fg_color"] if button_color is None else button_color 61 | self.text_color = customtkinter.ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else text_color 62 | 63 | if scrollbar is False: 64 | self.scroll_button_color = self.fg_color 65 | self.scroll_hover_color = self.fg_color 66 | 67 | self.frame_top = customtkinter.CTkFrame(self, bg_color=self.transparent_color, fg_color=self.fg_color, 68 | corner_radius=self.corner, border_width=frame_border_width, 69 | border_color=self.border_color) 70 | 71 | self.frame_top.pack(expand=True, fill="both") 72 | 73 | self.var = customtkinter.StringVar() 74 | self.var.trace_add('write', self.search) 75 | 76 | self.label = customtkinter.CTkLabel(self.frame_top, text=label) 77 | self.label.pack(fill="x", pady=10, padx=20) 78 | 79 | self.search_entry = customtkinter.CTkEntry(self.frame_top, fg_color=self.button_color, border_color=self.border_color, 80 | textvariable=self.var, **kwargs) 81 | 82 | self.search_entry.pack(fill="x", pady=0, padx=20) 83 | self.search_entry.bind("", lambda e: self.attach.unbind_all("")) 84 | 85 | self.frame = customtkinter.CTkScrollableFrame(self.frame_top, fg_color=self.fg_color, 86 | scrollbar_button_hover_color=self.scroll_hover_color, 87 | corner_radius=self.corner, scrollbar_button_color=self.scroll_button_color,) 88 | 89 | self.frame._scrollbar.grid_configure(padx=3) 90 | 91 | if self.padding: 92 | frame_padding = 10 93 | else: 94 | frame_padding = (0,6) 95 | 96 | self.frame.pack(expand=True, fill="both", padx=8, pady=frame_padding) 97 | self.no_match = customtkinter.CTkLabel(self.frame, text="No Match") 98 | 99 | if justify.lower()=="left": 100 | self.justify = "w" 101 | elif justify.lower()=="right": 102 | self.justify = "e" 103 | else: 104 | self.justify = "c" 105 | 106 | self.button_height = button_height 107 | 108 | self.resizable(width=False, height=False) 109 | self.disable = False 110 | 111 | self.attach.bind_all("", self.popup) 112 | 113 | self.update_idletasks() 114 | self.attach.focus_set() 115 | 116 | def _withdraw(self): 117 | if not self.disable: 118 | self.withdraw() 119 | self.attach.bind_all("", self.popup) 120 | 121 | def fade_out(self): 122 | for i in range(100,0,-10): 123 | if not self.winfo_exists(): 124 | break 125 | self.attributes("-alpha", i/100) 126 | self.update() 127 | time.sleep(1/100) 128 | 129 | def fade_in(self): 130 | for i in range(0,100,10): 131 | if not self.winfo_exists(): 132 | break 133 | self.attributes("-alpha", i/100) 134 | self.update() 135 | time.sleep(1/100) 136 | 137 | def search(self, a,b,c): 138 | self.live_update(self.var.get()) 139 | 140 | def add_node(self, label, command, **button_kwargs): 141 | self.node[self.button_num] = customtkinter.CTkButton(self.frame, 142 | text=label, 143 | text_color=self.text_color, 144 | height=self.button_height, 145 | fg_color=self.button_color, 146 | anchor=self.justify, 147 | command=lambda: self._attach_key_press(command), **button_kwargs) 148 | self.node[self.button_num].pack(fill="x", pady=5, padx=(self.padding,0)) 149 | self.button_num +=1 150 | 151 | def destroy_popup(self): 152 | self.destroy() 153 | self.disable = True 154 | 155 | def place_dropdown(self, x=None, y=None): 156 | self.geometry('{}x{}+{}+{}'.format(self.width, self.height, x, y)) 157 | self.fade_in() 158 | self.attributes('-alpha', self.alpha) 159 | 160 | def _iconify(self, x=None, y=None): 161 | self.focus_set() 162 | self._deiconify() 163 | if self.focus_something: self.node[0].focus_set() 164 | self.search_entry.focus_set() 165 | self.place_dropdown(x,y) 166 | 167 | def _attach_key_press(self, command): 168 | self.fade_out() 169 | self.withdraw() 170 | command() 171 | 172 | def live_update(self, string=None): 173 | if self.disable: return 174 | if self.fade: return 175 | if string: 176 | i=1 177 | for key in self.node.keys(): 178 | s = self.node[key].cget("text").lower() 179 | if not s.startswith(string.lower()): 180 | self.node[key].pack_forget() 181 | else: 182 | self.node[key].pack(fill="x", pady=5, padx=(self.padding,0)) 183 | i+=1 184 | 185 | if i==1: 186 | self.no_match.pack(fill="x", pady=2, padx=(self.padding,0)) 187 | else: 188 | self.no_match.pack_forget() 189 | self.button_num = i 190 | 191 | else: 192 | self.no_match.pack_forget() 193 | for key in self.node.keys(): 194 | self.node[key].pack(fill="x", pady=5, padx=(self.padding,0)) 195 | self.frame._parent_canvas.yview_moveto(0.0) 196 | 197 | def _deiconify(self): 198 | if self.button_num>0: 199 | self.deiconify() 200 | 201 | def popup(self, event): 202 | if self.disable: return 203 | self._iconify(event.x_root, event.y_root) 204 | 205 | def configure(self, **kwargs): 206 | if "height" in kwargs: 207 | self.height = kwargs.pop("height") 208 | 209 | if "alpha" in kwargs: 210 | self.alpha = kwargs.pop("alpha") 211 | 212 | if "width" in kwargs: 213 | self.width = kwargs.pop("width") 214 | 215 | if "fg_color" in kwargs: 216 | self.frame.configure(fg_color=kwargs.pop("fg_color")) 217 | 218 | if "button_color" in kwargs: 219 | for key in self.node.keys(): 220 | self.node[key].configure(fg_color=kwargs.pop("button_color")) 221 | 222 | for key in self.node.keys(): 223 | self.node[key].configure(**kwargs) 224 | -------------------------------------------------------------------------------- /tknodesystem/node_socket.py: -------------------------------------------------------------------------------- 1 | import tkinter 2 | 3 | class NodeSocket: 4 | def __init__(self, canvas, radius=10, center=(50,50), value=0, border_color='white', 5 | border_width=1, fg_color='green', hover_color='red', hover=True, socket_num=None): 6 | 7 | self.canvas = canvas 8 | self.radius = radius 9 | self.center = center 10 | self.value = value 11 | self.fg_color = fg_color 12 | self.hover_color = hover_color 13 | self.hover = hover 14 | self.hover_message = False 15 | self.live_connection = False 16 | 17 | self.create(border_color, border_width, self.fg_color) 18 | self.canvas.socket_num +=1 19 | if socket_num: 20 | self.socket_num = socket_num 21 | self.canvas.socket_num = socket_num 22 | else: 23 | self.socket_num = self.canvas.socket_num 24 | 25 | self.msg = tkinter.StringVar() 26 | self.hover_text = tkinter.Message(self.canvas, textvariable=self.msg, aspect=1000, 27 | highlightthickness=0, borderwidth=0, bg="grey20", fg="white") 28 | 29 | self.update() 30 | 31 | def create(self, border_color, border_width, connecter_color): 32 | """ create a circle which will act as a node socket """ 33 | 34 | self.ID = self.canvas.create_oval( 35 | (self.center[0]-self.radius, self.center[1]-self.radius), 36 | (self.center[0]+self.radius, self.center[1]+self.radius), 37 | outline=border_color, width=border_width, fill=connecter_color) 38 | 39 | self.canvas.tag_bind(self.ID, '', self.enter_socket) 40 | self.canvas.tag_bind(self.ID, '', self.leave_socket) 41 | 42 | def update(self): 43 | """ update the coordinates of socket """ 44 | try: 45 | self.cords = self.canvas.coords(self.ID) 46 | self.center = (self.cords[0]+self.cords[2])/2, (self.cords[1]+self.cords[3])/2 47 | except: None 48 | 49 | def enter_socket(self, event): 50 | if self.hover: self.canvas.itemconfigure(self.ID, fill=self.hover_color) 51 | if self.hover_message: 52 | x_pos = self.cords[0]-self.hover_text.winfo_reqwidth()-3 53 | y_pos = self.center[1]-self.hover_text.winfo_reqheight()/2 54 | self.hover_text.place(x=x_pos, y=y_pos) 55 | 56 | def leave_socket(self, event): 57 | if self.hover: self.canvas.itemconfigure(self.ID, fill=self.fg_color) 58 | if self.hover_message: self.hover_text.place_forget() 59 | 60 | def hide(self): 61 | self.canvas.itemconfigure(self.ID, state="hidden") 62 | 63 | def show(self): 64 | self.canvas.itemconfigure(self.ID, state="normal") 65 | 66 | def connect_wire(self): 67 | """ make a dummy wire """ 68 | if self.live_connection: return 69 | 70 | self.x1, self.y1 = self.center 71 | self.x2, self.y2 = self.center 72 | self.wire_exist = True 73 | self.wireID = self.canvas.create_line(self.x1, self.y1, self.x2, self.y2, dash=self.canvas.dash, 74 | width=self.canvas.wire_width, fill=self.canvas.wire_color) 75 | self.canvas.tag_lower(self.wireID) 76 | self.canvas.tag_lower(self.canvas.grid_bg) 77 | self.canvas.tag_bind(self.ID, "", lambda e: self.mouse_move(), add="+") 78 | self.canvas.tag_bind(self.wireID, "", lambda e: self.delete_wire(), add="+") 79 | if self.canvas.connect_wire: self.mouse_move() 80 | 81 | def delete_wire(self): 82 | """ delete the dummy wire """ 83 | self.canvas.delete(self.wireID) 84 | self.wire_exist = False 85 | self.live_connection = False 86 | 87 | def mouse_move(self): 88 | """ connect the dummy wire with mouse """ 89 | if self.ID not in self.canvas.find_all(): 90 | self.delete_wire() 91 | self.canvas.connect_wire = False 92 | if (self.x1, self.y1)!=self.center: 93 | self.delete_wire() 94 | x = self.canvas.master.winfo_pointerx() 95 | y = self.canvas.master.winfo_pointery() 96 | abs_coord_x = self.canvas.master.winfo_pointerx() - self.canvas.winfo_rootx() 97 | abs_coord_y = self.canvas.master.winfo_pointery() - self.canvas.winfo_rooty() 98 | 99 | self.canvas.coords(self.wireID, self.x1, self.y1, abs_coord_x, abs_coord_y) 100 | if self.wire_exist: 101 | self.canvas.after(50, self.mouse_move) 102 | self.live_connection = True 103 | else: 104 | self.live_connection = False 105 | self.canvas.connect_wire = True 106 | 107 | def configure(self, **kwargs): 108 | """ configure options """ 109 | 110 | if "fg_color" in kwargs: 111 | self.fg_color = kwargs.pop("fg_color") 112 | self.canvas.itemconfig(self.ID, fill=self.fg_color) 113 | if "hover_color" in kwargs: 114 | self.hover_color = kwargs.pop("hover_color") 115 | if "hover" in kwargs: 116 | self.hover = kwargs.pop("hover") 117 | 118 | if len(kwargs)>0: 119 | raise ValueError("This option is not configurable:" + list(kwargs.keys())[0]) 120 | 121 | -------------------------------------------------------------------------------- /tknodesystem/node_types.py: -------------------------------------------------------------------------------- 1 | import __main__ 2 | from .node import Node 3 | from .node_socket import NodeSocket 4 | from .node_args import Args 5 | import warnings 6 | 7 | class NodeValue(Node): 8 | def __init__(self, canvas, width=100, height=50, value=0, border_color='white', text=None, corner_radius=25, 9 | border_width=0, fg_color='#37373D', text_color='white', font=("",10), socket_radius=8, socket_hover=True, 10 | socket_color="green", socket_hover_color="grey50", highlightcolor='#52d66c', hover=True, justify="center", 11 | click_command=None, side="right", x=0, y=0, num=None, fixed=False): 12 | 13 | self.text = text 14 | self.canvas = canvas 15 | 16 | self.args = Args.value_args(locals()) 17 | 18 | if click_command: 19 | if click_command!="": 20 | if type(click_command) is str: 21 | click_command = getattr(__main__, click_command) 22 | self.args.update({"click_command": click_command.__name__}) 23 | else: 24 | click_command = None 25 | warnings.warn("Warning: currently cannot be saved and loaded, please use any local function instead") 26 | 27 | if self.text is None: 28 | self.text = f"Input{self.canvas.input_num}" 29 | 30 | self.canvas.input_num +=1 31 | self.connected_func = set() 32 | self.value = value 33 | self.type = 'NodeValue' 34 | 35 | if border_width==0: 36 | border_color = fg_color 37 | 38 | 39 | super().__init__(canvas=canvas, width=width, height=height, center=(width,height), text=str(self.text), 40 | border_width=border_width, border_color=border_color, fg_color=fg_color, corner_radius=corner_radius, 41 | text_color=text_color, font=font, highlightcolor=highlightcolor, hover=hover, justify=justify, 42 | click_command=click_command) 43 | 44 | if side=="left": 45 | center = (width-(width/2),height) 46 | else: 47 | center = (width+(width/2),height) 48 | 49 | self.output_ = NodeSocket(canvas, value=value, radius=socket_radius, center=center, 50 | fg_color=socket_color, hover_color=socket_hover_color, border_width=border_width, 51 | border_color=border_color, hover=socket_hover, socket_num=num) 52 | 53 | self.allIDs = self.allIDs + [self.output_.ID] 54 | 55 | self.bind_all_to_movement() 56 | self.canvas.tag_bind(self.output_.ID, '', self.connect_output) 57 | if not fixed: 58 | self.canvas.bind_all("", lambda e: self.destroy() if self.signal else None, add="+") 59 | self.socket_nums = self.output_.socket_num 60 | 61 | for j in range(self.canvas.gain_in): 62 | for i in self.allIDs: 63 | self.canvas.scale(i, 0, 0, 1.1, 1.1) 64 | 65 | for j in range(abs(self.canvas.gain_out)): 66 | for i in self.allIDs: 67 | self.canvas.scale(i, 0, 0, 0.9, 0.9) 68 | 69 | if x or y: 70 | super().move(x,y) 71 | 72 | self.canvas.obj_list.add(self) 73 | 74 | def get(self): 75 | """ get the current value of node """ 76 | return self.output_.value 77 | 78 | def connect_output(self, event): 79 | """ connect output socket """ 80 | 81 | self.canvas.clickcount += 1 82 | self.canvas.outputcell = self 83 | 84 | if self.canvas.clickcount == 2: 85 | self.canvas.clickcount = 0 86 | 87 | self.output_.connect_wire() 88 | 89 | def destroy(self): 90 | if self.ID not in self.canvas.find_all(): return 91 | 92 | self.output_.value = None 93 | for i in self.allIDs: 94 | self.canvas.delete(i) 95 | 96 | self.canvas.obj_list.remove(self) 97 | super().destroy() 98 | 99 | for i in self.connected_func: 100 | i.update() 101 | 102 | def exists(self): 103 | if self.ID in self.canvas.find_all(): 104 | return True 105 | else: 106 | return False 107 | 108 | def configure(self, **kwargs): 109 | """ configure options """ 110 | self.args.update(kwargs) 111 | if "value" in kwargs: 112 | self.output_.value = kwargs.pop("value") 113 | if not self.text: 114 | super().configure(text=kwargs.pop("text")) 115 | for i in self.connected_func: 116 | i.update() 117 | if "text" in kwargs: 118 | self.text = kwargs.pop("text") 119 | super().configure(text=self.text) 120 | if "fg_color" in kwargs: 121 | super().configure(fg_color=kwargs.pop("fg_color")) 122 | if "text_color" in kwargs: 123 | super().configure(text_color=kwargs.pop("text_color")) 124 | if "font" in kwargs: 125 | super().configure(font=kwargs.pop("font")) 126 | if "highlightcolor" in kwargs: 127 | super().configure(highlightcolor=kwargs.pop("highlightcolor")) 128 | if "socket_color" in kwargs: 129 | self.output_.configure(fg_color=kwargs.pop("socket_color")) 130 | if "socket_hover_color" in kwargs: 131 | self.output_.configure(hover_color=kwargs.pop("socket_hover_color")) 132 | if "hover" in kwargs: 133 | super().configure(hover=kwargs.pop("hover")) 134 | if "socket_hover" in kwargs: 135 | self.output_.configure(hover=kwargs.pop("socket_hover")) 136 | 137 | if len(kwargs)>0: 138 | raise ValueError("This option is not configurable:" + list(kwargs.keys())[0]) 139 | 140 | class NodeOperation(Node): 141 | def __init__(self, canvas, width=100, height=None, inputs=2, border_color='white', text=None, justify="center", hover_text=None, 142 | socket_radius=8, corner_radius=25, border_width=0, fg_color='#37373D', text_color='white', font=("",10), multiple_connection=False, 143 | highlightcolor='#52d66c', hover=True, socket_color="green", socket_hover_color="grey50", x=0, y=0, none_inputs=False, pass_node_id=False, 144 | multiside=False, command=None, output_socket_color="green", click_command=None, socket_hover=True, num=None, fixed=False): 145 | 146 | self.text = text 147 | self.canvas = canvas 148 | self.type = 'NodeOperation' 149 | self.hover_text = {} if hover_text is None else hover_text 150 | 151 | if self.text is None: 152 | self.text = f"Function{self.canvas.operation_num}" 153 | 154 | self.canvas.operation_num +=1 155 | 156 | if border_width==0: 157 | border_color = fg_color 158 | 159 | args = locals() 160 | args['hover_text'] = self.hover_text 161 | self.args = Args.func_args(args) 162 | 163 | self.pass_node = pass_node_id 164 | 165 | 166 | if command: 167 | if command!="": 168 | if type(command) is str: 169 | command = getattr(__main__, command) 170 | self.args.update({"command": command.__name__}) 171 | else: 172 | command = None 173 | warnings.warn("Warning: currently cannot be saved and loaded, please use any local function instead") 174 | if click_command: 175 | if click_command!="": 176 | if type(click_command) is str: 177 | click_command = getattr(__main__, click_command) 178 | self.args.update({"click_command": click_command.__name__}) 179 | else: 180 | click_command = None 181 | warnings.warn("Warning: currently cannot be saved and loaded, please use any local function instead") 182 | 183 | self.command = command 184 | 185 | if height is None: 186 | height = 50 + (inputs*10) 187 | 188 | super().__init__(canvas=canvas, width=width, height=height, center=(width,height), justify=justify, 189 | border_width=border_width, fg_color=fg_color, border_color=border_color, 190 | text_color=text_color, font=font, click_command=click_command, corner_radius=corner_radius, 191 | highlightcolor=highlightcolor, text=str(self.text), hover=hover) 192 | 193 | self.line1 = None 194 | self.line2 = None 195 | self.line3 = None 196 | self.line4 = None 197 | self.line5 = None 198 | self.socket_colors = [] 199 | self.connected_node = set() 200 | self.connected_node_first = None 201 | self.none_values = none_inputs 202 | 203 | x_pos = x 204 | y_pos = y 205 | 206 | if type(socket_color) is list: 207 | self.socket_colors = socket_color 208 | else: 209 | for i in range(5): 210 | self.socket_colors.append(socket_color) 211 | 212 | self.inputs = inputs 213 | 214 | if self.inputs>3 and self.height<=80: 215 | multiside=True 216 | 217 | if multiside: 218 | z = self.inputs-1 219 | else: 220 | z = self.inputs+1 221 | 222 | if z==0: 223 | z = 2 224 | 225 | y = height/z 226 | x = 1 227 | if self.inputs==2 and multiside: 228 | x = 1/2 229 | 230 | self.cellinput1 = None 231 | self.cellinput2 = None 232 | self.cellinput3 = None 233 | self.cellinput4 = None 234 | self.cellinput5 = None 235 | self.celloutput = None 236 | self.socket_nums = [] 237 | self.values_args = [] 238 | self.multiple = multiple_connection 239 | 240 | self.connected_inputs1 = list() 241 | self.connected_inputs2 = list() 242 | self.connected_inputs3 = list() 243 | self.connected_inputs4 = list() 244 | self.connected_inputs5 = list() 245 | 246 | self.output_ = NodeSocket(canvas, radius=socket_radius, center=(width+(width/2),height), 247 | fg_color=output_socket_color, hover_color=socket_hover_color, socket_num=num[0] if num else None, 248 | border_width=border_width, border_color=border_color, hover=socket_hover) 249 | self.canvas.tag_bind(self.output_.ID, '', self.connect_output) 250 | self.socket_nums.append(self.output_.socket_num) 251 | 252 | center = (width/2, y * x + height/2) 253 | self.input_1 = NodeSocket(canvas, radius=socket_radius, center=center, fg_color=self.socket_colors[0], 254 | hover_color=socket_hover_color, border_width=border_width, hover=socket_hover, 255 | border_color=border_color, socket_num=num[1] if num else None) 256 | 257 | id_list = [self.output_.ID, self.input_1.ID] 258 | self.canvas.tag_bind(self.input_1.ID, '', lambda e: self.connect_input(self.line1,'input1')) 259 | self.socket_nums.append(self.input_1.socket_num) 260 | 261 | if self.inputs>=2: 262 | if not multiside: 263 | x+=1 264 | center = (width/2,y * x +height/2) 265 | else: 266 | center = (width, height/2) 267 | 268 | self.input_2 = NodeSocket(canvas, radius=socket_radius, center=center, fg_color=self.socket_colors[1], 269 | hover_color=socket_hover_color, border_width=border_width, hover=socket_hover, 270 | border_color=border_color, socket_num=num[2] if num else None) 271 | id_list.append(self.input_2.ID) 272 | self.canvas.tag_bind(self.input_2.ID, '', lambda e: self.connect_input(self.line2,'input2')) 273 | self.socket_nums.append(self.input_2.socket_num) 274 | 275 | if self.inputs>=3: 276 | if not multiside: 277 | x+=1 278 | center = (width/2,y * x +height/2) 279 | else: 280 | center = (width, height*1.5) 281 | self.input_3 = NodeSocket(canvas, radius=socket_radius, center=center, fg_color=self.socket_colors[2], 282 | hover_color=socket_hover_color, border_width=border_width, hover=socket_hover, 283 | border_color=border_color, socket_num=num[3] if num else None) 284 | self.canvas.tag_bind(self.input_3.ID, '', lambda e: self.connect_input(self.line3,'input3')) 285 | id_list.append(self.input_3.ID) 286 | self.socket_nums.append(self.input_3.socket_num) 287 | 288 | if self.inputs>=4: 289 | x+=1 290 | center = (width/2,y * x +height/2) 291 | self.input_4 = NodeSocket(canvas, radius=socket_radius, center=center, fg_color=self.socket_colors[3], 292 | hover_color=socket_hover_color, border_width=border_width, hover=socket_hover, 293 | border_color=border_color, socket_num=num[4] if num else None) 294 | self.canvas.tag_bind(self.input_4.ID, '', lambda e: self.connect_input(self.line4,'input4')) 295 | id_list.append(self.input_4.ID) 296 | self.socket_nums.append(self.input_4.socket_num) 297 | 298 | if self.inputs>=5: 299 | x+=1 300 | center = (width/2,y * x +height/2) 301 | self.input_5 = NodeSocket(canvas, radius=socket_radius, center=center, fg_color=self.socket_colors[4], 302 | hover_color=socket_hover_color, border_width=border_width, hover=socket_hover, 303 | border_color=border_color, socket_num=num[5] if num else None) 304 | self.canvas.tag_bind(self.input_5.ID, '', lambda e: self.connect_input(self.line5, 'input5')) 305 | id_list.append(self.input_5.ID) 306 | self.socket_nums.append(self.input_5.socket_num) 307 | 308 | self.allIDs = self.allIDs + id_list 309 | self.bind_all_to_movement() 310 | self.id_list = id_list 311 | if not fixed: 312 | self.canvas.bind_all("", lambda e: self.destroy() if self.signal else None, add="+") 313 | 314 | for i in self.hover_text: 315 | self.config_socket(**self.hover_text[i]) 316 | 317 | for j in range(self.canvas.gain_in): 318 | for i in self.allIDs: 319 | self.canvas.scale(i, 0, 0, 1.1, 1.1) 320 | 321 | for j in range(abs(self.canvas.gain_out)): 322 | for i in self.allIDs: 323 | self.canvas.scale(i, 0, 0, 0.9, 0.9) 324 | 325 | if x_pos or y_pos: 326 | super().move(x_pos,y_pos) 327 | 328 | self.canvas.obj_list.add(self) 329 | 330 | def connect_output(self, event): 331 | """ connect output socket """ 332 | 333 | self.canvas.clickcount += 1 334 | self.canvas.outputcell = self 335 | 336 | if self.canvas.clickcount == 2: 337 | self.canvas.clickcount = 0 338 | 339 | self.output_.connect_wire() 340 | 341 | def connect_input(self, line_id, input_id): 342 | """ connect input sockets """ 343 | if self.canvas.outputcell is None: 344 | return 345 | if self.canvas.outputcell in self.connected_node: 346 | return 347 | if self.canvas.outputcell==self: 348 | return 349 | 350 | m = self.connected_node_first 351 | 352 | for i in range(self.canvas.operation_num): 353 | if m is None: 354 | break 355 | if m.type=="NodeOperation": 356 | if self.canvas.outputcell in m.connected_node: 357 | return 358 | m = m.connected_node_first 359 | 360 | if not self.multiple: 361 | try: self.canvas.delete(line_id.ID) 362 | except: None 363 | 364 | self.canvas.clickcount += 1 365 | self.canvas.IDc = input_id 366 | self.canvas.inputcell = self 367 | 368 | if self.canvas.clickcount == 2: 369 | self.canvas.clickcount = 0 370 | self.canvas.conectcells() 371 | if self.canvas.outputcell.type=="NodeValue": 372 | self.canvas.outputcell.connected_func.add(self) 373 | elif self.canvas.outputcell.type=="NodeOperation": 374 | self.canvas.outputcell.connected_node.add(self) 375 | self.canvas.outputcell.connected_node_first = self 376 | try: 377 | if input_id=="input1": 378 | if not self.multiple: 379 | for x in self.canvas.line_list: 380 | if x[1]==self.input_1.socket_num: 381 | self.canvas.line_list.remove(x) 382 | break 383 | else: 384 | if self.canvas.outputcell not in self.connected_inputs1: 385 | self.connected_inputs1.append(self.canvas.outputcell) 386 | l = (self.canvas.outputcell.output_.socket_num, self.canvas.inputcell.input_1.socket_num) 387 | self.canvas.line_list.add(l) 388 | if input_id=="input2": 389 | if not self.multiple: 390 | for x in self.canvas.line_list: 391 | if x[1]==self.input_2.socket_num: 392 | self.canvas.line_list.remove(x) 393 | break 394 | else: 395 | if self.canvas.outputcell not in self.connected_inputs2: 396 | self.connected_inputs2.append(self.canvas.outputcell) 397 | l = (self.canvas.outputcell.output_.socket_num, self.canvas.inputcell.input_2.socket_num) 398 | self.canvas.line_list.add(l) 399 | if input_id=="input3": 400 | if not self.multiple: 401 | for x in self.canvas.line_list: 402 | if x[1]==self.input_3.socket_num: 403 | self.canvas.line_list.remove(x) 404 | break 405 | else: 406 | if self.canvas.outputcell not in self.connected_inputs3: 407 | self.connected_inputs3.append(self.canvas.outputcell) 408 | l = (self.canvas.outputcell.output_.socket_num, self.canvas.inputcell.input_3.socket_num) 409 | self.canvas.line_list.add(l) 410 | if input_id=="input4": 411 | if not self.multiple: 412 | for x in self.canvas.line_list: 413 | if x[1]==self.input_4.socket_num: 414 | self.canvas.line_list.remove(x) 415 | break 416 | else: 417 | if self.canvas.outputcell not in self.connected_inputs4: 418 | self.connected_inputs4.append(self.canvas.outputcell) 419 | l = (self.canvas.outputcell.output_.socket_num, self.canvas.inputcell.input_4.socket_num) 420 | self.canvas.line_list.add(l) 421 | if input_id=="input5": 422 | if not self.multiple: 423 | for x in self.canvas.line_list: 424 | if x[1]==self.input_5.socket_num: 425 | self.canvas.line_list.remove(x) 426 | break 427 | else: 428 | if self.canvas.outputcell not in self.connected_inputs5: 429 | self.connected_inputs5.append(self.canvas.outputcell) 430 | l = (self.canvas.outputcell.output_.socket_num, self.canvas.inputcell.input_5.socket_num) 431 | self.canvas.line_list.add(l) 432 | except AttributeError: None 433 | 434 | else: 435 | if self.canvas.outputcell.type=="NodeValue": 436 | try: self.canvas.outputcell.connected_func.remove(self) 437 | except KeyError: None 438 | elif self.canvas.outputcell.type=="NodeOperation": 439 | try: 440 | self.canvas.outputcell.connected_node.remove(self) 441 | self.canvas.outputcell.connected_node_first = None 442 | except KeyError: None 443 | try: 444 | if input_id=="input1": 445 | self.cellinput1 = None 446 | self.canvas.line_list.remove((self.canvas.outputcell.output_.socket_num, self.canvas.inputcell.input_1.socket_num)) 447 | if self.multiple: 448 | self.connected_inputs1.remove(self.canvas.outputcell) 449 | if input_id=="input2": 450 | self.cellinput2 = None 451 | self.canvas.line_list.remove((self.canvas.outputcell.output_.socket_num, self.canvas.inputcell.input_2.socket_num)) 452 | if self.multiple: 453 | self.connected_inputs2.remove(self.canvas.outputcell) 454 | if input_id=="input3": 455 | self.cellinput3 = None 456 | self.canvas.line_list.remove((self.canvas.outputcell.output_.socket_num, self.canvas.inputcell.input_3.socket_num)) 457 | if self.multiple: 458 | self.connected_inputs3.remove(self.canvas.outputcell) 459 | if input_id=="input4": 460 | self.cellinput4 = None 461 | self.canvas.line_list.remove((self.canvas.outputcell.output_.socket_num, self.canvas.inputcell.input_4.socket_num)) 462 | if self.multiple: 463 | self.connected_inputs4.remove(self.canvas.outputcell) 464 | if input_id=="input5": 465 | self.cellinput5 = None 466 | self.canvas.line_list.remove((self.canvas.outputcell.output_.socket_num, self.canvas.inputcell.input_5.socket_num)) 467 | if multiple: 468 | self.connected_inputs5.remove(self.canvas.outputcell) 469 | except AttributeError: None 470 | except KeyError: None 471 | 472 | if self.multiple: 473 | for i in self.canvas.line_ids: 474 | if (i.firstcell==self.canvas.outputcell) and (i.secondcell==self) and (i.inputs==input_id): 475 | i.delete_line(i.inputs) 476 | break 477 | self.update() 478 | 479 | def toggle(self, input_num: int): 480 | line_num = eval(f"self.line{input_num}") 481 | input_num = f"input{input_num}" 482 | self.connect_input(line_num, input_num) 483 | 484 | def update(self): 485 | """ update the output values """ 486 | 487 | if self.multiple: 488 | cells = [self.connected_inputs1, 489 | self.connected_inputs2, 490 | self.connected_inputs3, 491 | self.connected_inputs4, 492 | self.connected_inputs5] 493 | self.values_args = [] 494 | 495 | if self.command: 496 | for i in cells[0:self.inputs]: 497 | v = [] 498 | for j in i: 499 | if j.output_.value is not None: 500 | v.append(j.output_.value) 501 | self.values_args.append(v) 502 | 503 | if not self.none_values: 504 | for i in self.values_args: 505 | if len(i)==0: 506 | self.output_.value = None 507 | break 508 | else: 509 | self.output_.value = 1 510 | else: 511 | self.output_.value = 1 512 | 513 | if self.output_.value: 514 | if self.pass_node: 515 | self.output_.value = self.command(self, *self.values_args[0:self.inputs]) 516 | else: 517 | self.output_.value = self.command(*self.values_args[0:self.inputs]) 518 | else: 519 | self.output_.value = [] 520 | 521 | else: 522 | arguments = [self.cellinput1, 523 | self.cellinput2, 524 | self.cellinput3, 525 | self.cellinput4, 526 | self.cellinput5] 527 | self.values_args = [] 528 | if self.command: 529 | for i in arguments[0:self.inputs]: 530 | if i is None: 531 | self.values_args.append(None) 532 | else: 533 | self.values_args.append(i.output_.value) 534 | 535 | if not self.none_values: 536 | for i in self.values_args: 537 | if i is None: 538 | self.output_.value = None 539 | break 540 | else: 541 | self.output_.value = 1 542 | else: 543 | self.output_.value = 1 544 | 545 | if self.output_.value: 546 | if self.pass_node: 547 | self.output_.value = self.command(self, *self.values_args[0:self.inputs]) 548 | else: 549 | self.output_.value = self.command(*self.values_args[0:self.inputs]) 550 | else: 551 | self.output_.value = None 552 | 553 | if len(self.connected_node)>0: 554 | for i in self.connected_node: 555 | i.update() 556 | 557 | def get(self): 558 | """ get the current value of node """ 559 | return self.output_.value 560 | 561 | def get_inputs(self): 562 | return self.values_args 563 | 564 | def destroy(self): 565 | if self.ID not in self.canvas.find_all(): return 566 | self.output_.value = None 567 | for i in self.id_list: 568 | self.canvas.delete(i) 569 | 570 | self.canvas.obj_list.remove(self) 571 | super().destroy() 572 | 573 | if len(self.connected_node)>0: 574 | for i in self.connected_node: 575 | i.update() 576 | 577 | def exists(self): 578 | if self.ID in self.canvas.find_all(): 579 | return True 580 | else: 581 | return False 582 | 583 | def config_socket(self, index: int, hover_text: str=None, hover_text_color=None, hover_bg=None, **kwargs): 584 | if index==1: 585 | socket = self.input_1 586 | elif index==2: 587 | socket = self.input_2 588 | elif index==3: 589 | socket = self.input_3 590 | elif index==4: 591 | socket = self.input_4 592 | elif index==5: 593 | socket = self.input_5 594 | else: 595 | return 596 | 597 | kwarg_args = {} 598 | for i in kwargs: 599 | kwarg_args = {i: kwargs[i]} 600 | 601 | self.hover_text[str(index)] = {'index': index, 'hover_text': hover_text, 'hover_text_color': hover_text_color, 'hover_bg': hover_bg, **kwarg_args} 602 | 603 | if hover_text: 604 | socket.hover_message = True 605 | socket.msg.set(hover_text) 606 | else: 607 | socket.hover_message = False 608 | 609 | if hover_text_color: 610 | socket.hover_text.configure(fg=hover_text_color) 611 | 612 | if hover_bg: 613 | socket.hover_text.configure(bg=hover_bg) 614 | 615 | if "socket_color" in kwargs: 616 | socket.configure(fg_color=kwargs.pop("socket_color")) 617 | 618 | self.configure(**kwargs) 619 | 620 | def configure(self, **kwargs): 621 | """ configure options """ 622 | self.args.update(kwargs) 623 | if "text" in kwargs: 624 | self.text = kwargs.pop("text") 625 | super().configure(text=self.text) 626 | if "fg_color" in kwargs: 627 | super().configure(fg_color=kwargs.pop("fg_color")) 628 | if "text_color" in kwargs: 629 | super().configure(text_color=kwargs.pop("text_color")) 630 | if "font" in kwargs: 631 | super().configure(font=kwargs.pop("font")) 632 | if "highlightcolor" in kwargs: 633 | super().configure(highlightcolor=kwargs.pop("highlightcolor")) 634 | if "socket_color" in kwargs: 635 | socket_color = kwargs.pop("socket_color") 636 | if type(socket_color) is list: 637 | try: 638 | self.input_1.configure(fg_color=socket_color[0]) 639 | self.input_2.configure(fg_color=socket_color[1]) 640 | self.input_3.configure(fg_color=socket_color[2]) 641 | self.input_4.configure(fg_color=socket_color[3]) 642 | self.input_5.configure(fg_color=socket_color[4]) 643 | except: None 644 | else: 645 | try: 646 | self.input_1.configure(fg_color=socket_color) 647 | self.input_2.configure(fg_color=socket_color) 648 | self.input_3.configure(fg_color=socket_color) 649 | self.input_4.configure(fg_color=socket_color) 650 | self.input_5.configure(fg_color=socket_color) 651 | except: None 652 | 653 | if "socket_hover_color" in kwargs: 654 | socket_hover_color = kwargs.pop("socket_hover_color") 655 | self.output_.configure(hover_color=socket_hover_color) 656 | try: 657 | self.input_1.configure(hover_color=socket_hover_color) 658 | self.input_2.configure(hover_color=socket_hover_color) 659 | self.input_3.configure(hover_color=socket_hover_color) 660 | self.input_4.configure(hover_color=socket_hover_color) 661 | self.input_5.configure(hover_color=socket_hover_color) 662 | except: None 663 | 664 | if "output_socket_color" in kwargs: 665 | self.output_.configure(fg_color=kwargs.pop("output_socket_color")) 666 | if "hover" in kwargs: 667 | super().configure(hover=kwargs.pop("hover")) 668 | if "socket_hover" in kwargs: 669 | hover = kwargs.pop("socket_hover") 670 | self.output_.configure(hover=hover) 671 | try: 672 | self.input_1.configure(hover=hover) 673 | self.input_2.configure(hover=hover) 674 | self.input_3.configure(hover=hover) 675 | self.input_4.configure(hover=hover) 676 | self.input_5.configure(hover=hover) 677 | except: None 678 | 679 | if len(kwargs)>0: 680 | raise ValueError("This option is not configurable:" + list(kwargs.keys())[0]) 681 | 682 | class NodeCompile(Node): 683 | def __init__(self, canvas, width=100, height=50, border_color='white', text="Compile", socket_radius=8, corner_radius=25, x=0, y=0, justify="center", 684 | border_width=0, fg_color='#37373D',text_color='white', font=("",10), highlightcolor='#52d66c', hover=True, socket_hover=True, fixed=False, 685 | socket_color="green", socket_hover_color="grey50", show_value=True, command=None, click_command=None, side="left", num=None, 686 | multiple_connection=False, pass_node_id=False): 687 | 688 | self.canvas = canvas 689 | self.text = text 690 | self.type = 'NodeCompile' 691 | 692 | if border_width==0: 693 | border_color = fg_color 694 | 695 | self.canvas.compile_num +=1 696 | 697 | self.args = Args.compile_args(locals()) 698 | 699 | self.pass_node = pass_node_id 700 | 701 | if command: 702 | if command!="": 703 | if type(command) is str: 704 | command = getattr(__main__, command) 705 | self.args.update({"command": command.__name__}) 706 | else: 707 | command = None 708 | warnings.warn("Warning: currently cannot be saved and loaded, please use any local function instead") 709 | if click_command: 710 | if click_command!="": 711 | if type(click_command) is str: 712 | click_command = getattr(__main__, click_command) 713 | self.args.update({"click_command": click_command.__name__}) 714 | else: 715 | click_command = None 716 | warnings.warn("Warning: currently cannot be saved and loaded, please use any local function instead") 717 | 718 | super().__init__(canvas=canvas, width=width, height=height, center=(width,height), text=str(text), corner_radius=corner_radius, 719 | border_width=border_width, fg_color=fg_color, border_color=border_color, hover=hover, justify=justify, 720 | text_color=text_color, font=font, click_command=click_command, highlightcolor=highlightcolor) 721 | 722 | self.line1 = None 723 | self.cellinput1 = None 724 | self.celloutput = None 725 | self.show_value = show_value 726 | self.previous_value = None 727 | self.command = command 728 | self.connected_inputs = list() 729 | self.multiple = multiple_connection 730 | self.multilines = {} 731 | 732 | if side=="left": 733 | center = (width-(width/2),height) 734 | else: 735 | center = (width+(width/2),height) 736 | 737 | self.input_1 = NodeSocket(canvas, radius=socket_radius, center=center, border_width=border_width, 738 | fg_color=socket_color, hover_color=socket_hover_color, border_color=border_color, 739 | hover=socket_hover, socket_num=num[0] if num else None) 740 | 741 | self.output_ = NodeSocket(canvas, radius=socket_radius, center=(width+(width/2),height), 742 | fg_color=socket_color, hover_color=socket_hover_color, border_width=border_width, 743 | border_color=border_color, hover=socket_hover, socket_num=num[1] if num else None) 744 | 745 | self.socket_nums = [self.input_1.socket_num, self.output_.socket_num] 746 | self.allIDs = self.allIDs + [self.output_.ID, self.input_1.ID] 747 | self.fixed = True 748 | self.bind_all_to_movement() 749 | self.canvas.tag_bind(self.input_1.ID, '', self.connect_input) 750 | 751 | if not fixed: 752 | self.canvas.bind_all("", lambda e: self.destroy() if self.signal else None, add="+") 753 | 754 | self.output_.hide() 755 | 756 | for j in range(self.canvas.gain_in): 757 | for i in self.allIDs: 758 | self.canvas.scale(i, 0, 0, 1.1, 1.1) 759 | 760 | for j in range(abs(self.canvas.gain_out)): 761 | for i in self.allIDs: 762 | self.canvas.scale(i, 0, 0, 0.9, 0.9) 763 | 764 | if x or y: 765 | super().move(x,y) 766 | 767 | self.canvas.obj_list.add(self) 768 | 769 | def connect_output(self, event): 770 | """ connect output socket """ 771 | 772 | self.canvas.clickcount += 1 773 | self.canvas.outputcell = self 774 | 775 | if self.canvas.clickcount == 2: 776 | self.canvas.clickcount = 0 777 | 778 | self.output_.connect_wire() 779 | 780 | def connect_input(self, event): 781 | """ connect input sockets """ 782 | 783 | if not self.multiple: 784 | try: self.canvas.delete(self.line1.ID) 785 | except: None 786 | else: 787 | if self.canvas.outputcell in list(self.multilines.keys()): 788 | if self.multilines[self.canvas.outputcell] is not None: 789 | self.canvas.delete(self.multilines[self.canvas.outputcell]) 790 | del self.multilines[self.canvas.outputcell] 791 | 792 | self.canvas.clickcount += 1 793 | self.canvas.IDc = 'input1' 794 | self.canvas.inputcell = self 795 | 796 | if self.canvas.clickcount == 2: 797 | self.canvas.clickcount = 0 798 | self.canvas.conectcells() 799 | self.fixed = False 800 | try: 801 | if self.canvas.outputcell.type=="NodeValue": 802 | self.canvas.outputcell.connected_func.add(self) 803 | if not self.multiple: 804 | for x in self.canvas.line_list: 805 | if x[1]==self.input_1.socket_num: 806 | self.canvas.line_list.remove(x) 807 | break 808 | else: 809 | if self.canvas.outputcell not in self.connected_inputs: 810 | self.connected_inputs.append(self.canvas.outputcell) 811 | self.canvas.line_list.add((self.canvas.outputcell.output_.socket_num, self.canvas.inputcell.input_1.socket_num)) 812 | except AttributeError: None 813 | else: 814 | self.fixed = True 815 | try: 816 | if self.canvas.outputcell.type=="NodeValue": 817 | try: self.canvas.outputcell.connected_func.remove(self) 818 | except KeyError: None 819 | self.canvas.line_list.remove((self.canvas.outputcell.output_.socket_num, self.canvas.inputcell.input_1.socket_num)) 820 | if self.multiple: 821 | self.connected_inputs.remove(self.canvas.outputcell) 822 | self.canvas.delete(self.line1.ID) 823 | except AttributeError: None 824 | except KeyError: None 825 | if self.multiple: 826 | try: 827 | self.multilines.update({self.canvas.outputcell:self.line1.ID}) 828 | except: 829 | self.multilines.update({self.canvas.outputcell:None}) 830 | 831 | def get(self): 832 | """ get the current value of node """ 833 | return self.output_.value 834 | 835 | def toggle(self): 836 | self.connect_input(0) 837 | 838 | def update(self): 839 | """ update the output values """ 840 | 841 | if self.ID not in self.canvas.find_all(): 842 | return 843 | if self.multiple: 844 | output = [] 845 | for i in self.connected_inputs: 846 | if i.output_.value is not None: 847 | output.append(i.output_.value) 848 | else: 849 | output = self.cellinput1.output_.value if not self.fixed else None 850 | 851 | self.output_.value = output 852 | if self.previous_value!=self.output_.value: 853 | if self.show_value: 854 | self.canvas.itemconfigure(self.IDtext, text=str(self.output_.value)) 855 | if self.output_.value is not None: 856 | if self.command: 857 | if self.pass_node: 858 | self.command(self, self.output_.value) 859 | else: 860 | self.command(self.output_.value) 861 | self.previous_value = self.output_.value 862 | 863 | self.canvas.after(50, self.update) 864 | 865 | def destroy(self): 866 | if self.ID not in self.canvas.find_all(): return 867 | self.output_.value = None 868 | 869 | for i in self.allIDs: 870 | self.canvas.delete(i) 871 | 872 | self.canvas.obj_list.remove(self) 873 | super().destroy() 874 | 875 | def exists(self): 876 | if self.ID in self.canvas.find_all(): 877 | return True 878 | else: 879 | return False 880 | 881 | def configure(self, **kwargs): 882 | """ configure options """ 883 | self.args.update(kwargs) 884 | if "text" in kwargs: 885 | self.text = kwargs.pop("text") 886 | super().configure(text=self.text) 887 | if "fg_color" in kwargs: 888 | super().configure(fg_color=kwargs.pop("fg_color")) 889 | if "text_color" in kwargs: 890 | super().configure(text_color=kwargs.pop("text_color")) 891 | if "font" in kwargs: 892 | super().configure(font=kwargs.pop("font")) 893 | if "highlightcolor" in kwargs: 894 | super().configure(highlightcolor=kwargs.pop("highlightcolor")) 895 | if "socket_color" in kwargs: 896 | self.input_1.configure(fg_color=kwargs.pop("socket_color")) 897 | if "socket_hover_color" in kwargs: 898 | self.input_1.configure(hover_color=kwargs.pop("socket_hover_color")) 899 | if "hover" in kwargs: 900 | super().configure(hover=kwargs.pop("hover")) 901 | if "socket_hover" in kwargs: 902 | self.input_1.configure(hover=kwargs.pop("socket_hover")) 903 | if "show_value" in kwargs: 904 | self.show_value = kwargs.pop("show_value") 905 | 906 | if len(kwargs)>0: 907 | raise ValueError("This option is not configurable:" + list(kwargs.keys())[0]) 908 | -------------------------------------------------------------------------------- /tknodesystem/node_wire.py: -------------------------------------------------------------------------------- 1 | class NodeWire(): 2 | def __init__(self, canvas, firstcell, secondcell, wire_color='white', wire_width=3, 3 | dash=True, wire_hover_color="red"): 4 | 5 | self.canvas = canvas 6 | self.firstcell = firstcell # output socket 7 | self.secondcell = secondcell # input socket 8 | self.IDc = self.canvas.IDc 9 | self.wire_color = wire_color 10 | self.wire_width = wire_width 11 | self.canvas.line_ids.add(self) 12 | self.dash = (2,4) if dash else () 13 | self.hover_color = wire_hover_color 14 | self.connected = True 15 | 16 | if self.firstcell: 17 | self.create() 18 | self.update() 19 | 20 | def create(self): 21 | """ creates the line from output-->input sockets """ 22 | 23 | self.firstcell.line = self 24 | self.x1, self.y1 = self.firstcell.output_.center 25 | 26 | if self.canvas.IDc == 'input1': 27 | self.input_num = self.secondcell.input_1 28 | self.secondcell.line1 = self 29 | 30 | if self.canvas.IDc == 'input2': 31 | self.input_num = self.secondcell.input_2 32 | self.secondcell.line2 = self 33 | 34 | if self.canvas.IDc == 'input3': 35 | self.input_num = self.secondcell.input_3 36 | self.secondcell.line3 = self 37 | 38 | if self.canvas.IDc == 'input4': 39 | self.input_num = self.secondcell.input_4 40 | self.secondcell.line4 = self 41 | 42 | if self.canvas.IDc == 'input5': 43 | self.input_num = self.secondcell.input_5 44 | self.secondcell.line5 = self 45 | 46 | self.x2,self.y2 = self.input_num.center 47 | self.firstcell.output_.delete_wire() 48 | self.ID = self.canvas.create_line(self.x1, self.y1, self.x2, self.y2, dash=self.dash, width=self.wire_width, 49 | activefill=self.hover_color, fill=self.wire_color, activewidth=self.wire_width) 50 | self.canvas.tag_lower(self.ID) 51 | self.canvas.tag_lower(self.canvas.grid_bg) 52 | self.inputs = self.canvas.IDc 53 | self.canvas.tag_bind(self.ID, "", lambda e: self.delete_line(self.inputs)) 54 | 55 | def delete_line(self, input_num): 56 | """ delete the line when disconnected """ 57 | self.canvas.delete(self.ID) 58 | self.canvas.line_ids.remove(self) 59 | self.firstcell.output_.live_connection = True 60 | self.firstcell.connect_output(None) 61 | self.firstcell.output_.live_connection = False 62 | self.canvas.clickcount = 0 63 | if self.secondcell.type=="NodeOperation": 64 | self.secondcell.connect_input(self.ID, input_num) 65 | else: 66 | self.secondcell.connect_input(None) 67 | 68 | def update(self): 69 | """ update the coordinates of line based on the socket position """ 70 | all_items = self.canvas.find_all() 71 | if self.firstcell.ID not in all_items or self.secondcell.ID not in all_items: 72 | self.canvas.delete(self.ID) 73 | self.connected = False 74 | if self.ID not in all_items: return 75 | self.x1, self.y1 = self.firstcell.output_.center 76 | if self.IDc == 'input1': self.x2, self.y2 = self.secondcell.input_1.center 77 | if self.IDc == 'input2': self.x2, self.y2 = self.secondcell.input_2.center 78 | if self.IDc == 'input3': self.x2, self.y2 = self.secondcell.input_3.center 79 | if self.IDc == 'input4': self.x2, self.y2 = self.secondcell.input_4.center 80 | if self.IDc == 'input5': self.x2, self.y2 = self.secondcell.input_5.center 81 | self.canvas.coords(self.ID, self.x1, self.y1, self.x2, self.y2) 82 | 83 | def configure(self, **kwargs): 84 | if "wire_color" in kwargs: 85 | self.canvas.itemconfig(self.ID, fill=kwargs.pop("wire_color")) 86 | if "wire_width" in kwargs: 87 | self.canvas.itemconfig(self.ID, width=kwargs.pop("wire_width")) 88 | if "dash" in kwargs: 89 | self.dash = kwargs.pop("dash") 90 | self.canvas.itemconfig(self.ID, dash=self.dash if self.dash else ()) 91 | if "wire_hover_color" in kwargs: 92 | self.canvas.itemconfig(self.ID, activefill=kwargs.pop("wire_hover_color")) 93 | 94 | if len(kwargs)>0: 95 | raise ValueError("This option is not configurable:" + list(kwargs.keys())[0]) 96 | 97 | --------------------------------------------------------------------------------