├── MANIFEST.in ├── .gitignore ├── requirements.txt ├── Screenshots └── NodeEditor1.png ├── Panda3DNodeEditor ├── icons │ ├── Plug.png │ ├── PlugConnectedGood.png │ └── Plug.svg ├── GUI │ ├── __init__.py │ ├── MainView.py │ └── MenuBar.py ├── __init__.py ├── NodeCore │ ├── __init__.py │ ├── Sockets │ │ ├── __init__.py │ │ ├── InSocket.py │ │ ├── OutSocket.py │ │ ├── BoolSocket.py │ │ ├── OptionSelectSocket.py │ │ ├── SocketBase.py │ │ ├── TextSocket.py │ │ └── NumericSocket.py │ ├── Nodes │ │ ├── __init__.py │ │ ├── NumericNode.py │ │ ├── BoolNode.py │ │ ├── AddNode.py │ │ ├── BoolAnd.py │ │ ├── BoolOr.py │ │ ├── MultiplyNode.py │ │ ├── DivideNode.py │ │ ├── TestOutNode.py │ │ └── NodeBase.py │ ├── NodeConnector.py │ └── NodeManager.py ├── Tools │ ├── __init__.py │ └── JSONTools.py ├── LoadScripts │ ├── __init__.py │ └── LoadJSON.py ├── SaveScripts │ ├── __init__.py │ └── SaveJSON.py └── NodeEditor.py ├── setup.py ├── .github └── workflows │ └── python-publish.yml ├── main.py ├── LICENSE ├── editorLogHandler.py └── README.md /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include Panda3DNodeEditor/icons/*.png 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | /build 3 | /dist 4 | /*.egg-info 5 | *.py[cod] 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | panda3d 2 | DirectFolderBrowser>=22.10 3 | DirectGuiExtension>=22.10 4 | -------------------------------------------------------------------------------- /Screenshots/NodeEditor1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireclawthefox/NodeEditor/HEAD/Screenshots/NodeEditor1.png -------------------------------------------------------------------------------- /Panda3DNodeEditor/icons/Plug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireclawthefox/NodeEditor/HEAD/Panda3DNodeEditor/icons/Plug.png -------------------------------------------------------------------------------- /Panda3DNodeEditor/icons/PlugConnectedGood.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireclawthefox/NodeEditor/HEAD/Panda3DNodeEditor/icons/PlugConnectedGood.png -------------------------------------------------------------------------------- /Panda3DNodeEditor/GUI/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | __author__ = "Fireclaw the Fox" 4 | __license__ = """ 5 | Simplified BSD (BSD 2-Clause) License. 6 | See License.txt or http://opensource.org/licenses/BSD-2-Clause for more info 7 | """ 8 | 9 | 10 | -------------------------------------------------------------------------------- /Panda3DNodeEditor/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | __author__ = "Fireclaw the Fox" 4 | __license__ = """ 5 | Simplified BSD (BSD 2-Clause) License. 6 | See License.txt or http://opensource.org/licenses/BSD-2-Clause for more info 7 | """ 8 | 9 | 10 | -------------------------------------------------------------------------------- /Panda3DNodeEditor/NodeCore/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | __author__ = "Fireclaw the Fox" 4 | __license__ = """ 5 | Simplified BSD (BSD 2-Clause) License. 6 | See License.txt or http://opensource.org/licenses/BSD-2-Clause for more info 7 | """ 8 | 9 | 10 | -------------------------------------------------------------------------------- /Panda3DNodeEditor/Tools/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | __author__ = "Fireclaw the Fox" 4 | __license__ = """ 5 | Simplified BSD (BSD 2-Clause) License. 6 | See License.txt or http://opensource.org/licenses/BSD-2-Clause for more info 7 | """ 8 | 9 | 10 | -------------------------------------------------------------------------------- /Panda3DNodeEditor/LoadScripts/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | __author__ = "Fireclaw the Fox" 4 | __license__ = """ 5 | Simplified BSD (BSD 2-Clause) License. 6 | See License.txt or http://opensource.org/licenses/BSD-2-Clause for more info 7 | """ 8 | 9 | 10 | -------------------------------------------------------------------------------- /Panda3DNodeEditor/SaveScripts/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | __author__ = "Fireclaw the Fox" 4 | __license__ = """ 5 | Simplified BSD (BSD 2-Clause) License. 6 | See License.txt or http://opensource.org/licenses/BSD-2-Clause for more info 7 | """ 8 | 9 | 10 | -------------------------------------------------------------------------------- /Panda3DNodeEditor/NodeCore/Sockets/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | __author__ = "Fireclaw the Fox" 4 | __license__ = """ 5 | Simplified BSD (BSD 2-Clause) License. 6 | See License.txt or http://opensource.org/licenses/BSD-2-Clause for more info 7 | """ 8 | 9 | 10 | -------------------------------------------------------------------------------- /Panda3DNodeEditor/NodeCore/Nodes/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | __author__ = "Fireclaw the Fox" 4 | __license__ = """ 5 | Simplified BSD (BSD 2-Clause) License. 6 | See License.txt or http://opensource.org/licenses/BSD-2-Clause for more info 7 | """ 8 | 9 | from os.path import dirname, basename, isfile, join 10 | import glob 11 | modules = glob.glob(join(dirname(__file__), "*.py")) 12 | __all__ = [ basename(f)[:-3] for f in modules if isfile(f) and not f.endswith('__init__.py')] 13 | -------------------------------------------------------------------------------- /Panda3DNodeEditor/NodeCore/Nodes/NumericNode.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | __author__ = "Fireclaw the Fox" 4 | __license__ = """ 5 | Simplified BSD (BSD 2-Clause) License. 6 | See License.txt or http://opensource.org/licenses/BSD-2-Clause for more info 7 | """ 8 | 9 | from Panda3DNodeEditor.NodeCore.Nodes.NodeBase import NodeBase 10 | from Panda3DNodeEditor.NodeCore.Sockets.NumericSocket import NumericSocket 11 | 12 | class Node(NodeBase): 13 | def __init__(self, parent): 14 | NodeBase.__init__(self, "IN", parent) 15 | self.addOut("Out") 16 | self.addIn("In", NumericSocket) 17 | 18 | def logic(self): 19 | self.outputList[0].value = self.inputList[0].getValue() 20 | -------------------------------------------------------------------------------- /Panda3DNodeEditor/NodeCore/Nodes/BoolNode.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | __author__ = "Fireclaw the Fox" 4 | __license__ = """ 5 | Simplified BSD (BSD 2-Clause) License. 6 | See License.txt or http://opensource.org/licenses/BSD-2-Clause for more info 7 | """ 8 | 9 | from Panda3DNodeEditor.NodeCore.Nodes.NodeBase import NodeBase 10 | from Panda3DNodeEditor.NodeCore.Sockets.BoolSocket import BoolSocket 11 | 12 | class Node(NodeBase): 13 | def __init__(self, parent): 14 | NodeBase.__init__(self, "BOOL", parent) 15 | self.addOut("Out") 16 | self.addIn("", BoolSocket) 17 | 18 | def logic(self): 19 | self.outputList[0].value = self.inputList[0].getValue() 20 | -------------------------------------------------------------------------------- /Panda3DNodeEditor/NodeCore/Nodes/AddNode.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | __author__ = "Fireclaw the Fox" 4 | __license__ = """ 5 | Simplified BSD (BSD 2-Clause) License. 6 | See License.txt or http://opensource.org/licenses/BSD-2-Clause for more info 7 | """ 8 | 9 | from Panda3DNodeEditor.NodeCore.Nodes.NodeBase import NodeBase 10 | from Panda3DNodeEditor.NodeCore.Sockets.InSocket import InSocket 11 | 12 | class Node(NodeBase): 13 | def __init__(self, parent): 14 | NodeBase.__init__(self, "ADD", parent) 15 | self.addOut("Out") 16 | self.addIn("In 1", InSocket) 17 | self.addIn("In 2", InSocket) 18 | 19 | def logic(self): 20 | if self.inputList[0].value is None or self.inputList[1].value is None: 21 | self.outputList[0].value = float("NaN") 22 | return 23 | self.outputList[0].value = self.inputList[0].value + self.inputList[1].value 24 | -------------------------------------------------------------------------------- /Panda3DNodeEditor/NodeCore/Nodes/BoolAnd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | __author__ = "Fireclaw the Fox" 4 | __license__ = """ 5 | Simplified BSD (BSD 2-Clause) License. 6 | See License.txt or http://opensource.org/licenses/BSD-2-Clause for more info 7 | """ 8 | 9 | from Panda3DNodeEditor.NodeCore.Nodes.NodeBase import NodeBase 10 | from Panda3DNodeEditor.NodeCore.Sockets.InSocket import InSocket 11 | 12 | class Node(NodeBase): 13 | def __init__(self, parent): 14 | NodeBase.__init__(self, "AND", parent) 15 | self.addOut("Out") 16 | self.addIn("In 1", InSocket) 17 | self.addIn("In 2", InSocket) 18 | 19 | def logic(self): 20 | if self.inputList[0].value is None or self.inputList[1].value is None: 21 | self.outputList[0].value = None 22 | return 23 | self.outputList[0].value = self.inputList[0].value and self.inputList[1].value 24 | -------------------------------------------------------------------------------- /Panda3DNodeEditor/NodeCore/Nodes/BoolOr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | __author__ = "Fireclaw the Fox" 4 | __license__ = """ 5 | Simplified BSD (BSD 2-Clause) License. 6 | See License.txt or http://opensource.org/licenses/BSD-2-Clause for more info 7 | """ 8 | 9 | from Panda3DNodeEditor.NodeCore.Nodes.NodeBase import NodeBase 10 | from Panda3DNodeEditor.NodeCore.Sockets.InSocket import InSocket 11 | 12 | class Node(NodeBase): 13 | def __init__(self, parent): 14 | NodeBase.__init__(self, "OR", parent) 15 | self.addOut("Out") 16 | self.addIn("In 1", InSocket) 17 | self.addIn("In 2", InSocket) 18 | 19 | def logic(self): 20 | if self.inputList[0].value is None or self.inputList[1].value is None: 21 | self.outputList[0].value = None 22 | return 23 | self.outputList[0].value = self.inputList[0].value or self.inputList[1].value 24 | -------------------------------------------------------------------------------- /Panda3DNodeEditor/NodeCore/Nodes/MultiplyNode.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | __author__ = "Fireclaw the Fox" 4 | __license__ = """ 5 | Simplified BSD (BSD 2-Clause) License. 6 | See License.txt or http://opensource.org/licenses/BSD-2-Clause for more info 7 | """ 8 | 9 | from Panda3DNodeEditor.NodeCore.Nodes.NodeBase import NodeBase 10 | from Panda3DNodeEditor.NodeCore.Sockets.InSocket import InSocket 11 | 12 | class Node(NodeBase): 13 | def __init__(self, parent): 14 | NodeBase.__init__(self, "MULTIPLY", parent) 15 | self.addOut("Out") 16 | self.addIn("In 1", InSocket) 17 | self.addIn("In 2", InSocket) 18 | 19 | def logic(self): 20 | """Multiplies the values given in the into nodes if both are set""" 21 | if self.inputList[0].value is None or self.inputList[1].value is None: 22 | self.outputList[0].value = float("NaN") 23 | return 24 | self.outputList[0].value = self.inputList[0].value * self.inputList[1].value 25 | -------------------------------------------------------------------------------- /Panda3DNodeEditor/NodeCore/Nodes/DivideNode.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | __author__ = "Fireclaw the Fox" 4 | __license__ = """ 5 | Simplified BSD (BSD 2-Clause) License. 6 | See License.txt or http://opensource.org/licenses/BSD-2-Clause for more info 7 | """ 8 | 9 | from Panda3DNodeEditor.NodeCore.Nodes.NodeBase import NodeBase 10 | from Panda3DNodeEditor.NodeCore.Sockets.InSocket import InSocket 11 | 12 | class Node(NodeBase): 13 | def __init__(self, parent): 14 | NodeBase.__init__(self, "DIVIDE", parent) 15 | self.addOut("Out") 16 | self.addIn("In 1", InSocket) 17 | self.addIn("In 2", InSocket) 18 | 19 | def logic(self): 20 | """Divides the values given in the into nodes if both are set""" 21 | if self.inputList[0].value is None or self.inputList[1].value is None: 22 | self.outputList[0].value = float("NaN") 23 | return 24 | if self.inputList[1].value != 0: 25 | self.outputList[0].value = self.inputList[0].value / self.inputList[1].value 26 | else: 27 | self.outputList[0].value = float("NaN") 28 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="Panda3DNodeEditor", 8 | version="22.05", 9 | author="Fireclaw", 10 | author_email="fireclawthefox@gmail.com", 11 | description="A node editor for the Panda3D engine", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/fireclawthefox/NodeEditor", 15 | packages=setuptools.find_packages(), 16 | include_package_data=True, 17 | classifiers=[ 18 | "Programming Language :: Python :: 3", 19 | "License :: OSI Approved :: BSD License", 20 | "Operating System :: OS Independent", 21 | "Development Status :: 2 - Pre-Alpha", 22 | "Intended Audience :: Developers", 23 | "Intended Audience :: End Users/Desktop", 24 | "Topic :: Multimedia :: Graphics", 25 | "Topic :: Multimedia :: Graphics :: Editors", 26 | ], 27 | install_requires=[ 28 | 'panda3d', 29 | 'DirectFolderBrowser', 30 | 'DirectGuiExtension' 31 | ], 32 | python_requires='>=3.6', 33 | ) 34 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Set up Python 26 | uses: actions/setup-python@v3 27 | with: 28 | python-version: '3.x' 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install build 33 | - name: Build package 34 | run: python -m build 35 | - name: Publish package 36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 37 | with: 38 | user: __token__ 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from direct.showbase.ShowBase import ShowBase 5 | from panda3d.core import ( 6 | loadPrcFileData, 7 | WindowProperties, 8 | AntialiasAttrib) 9 | from editorLogHandler import setupLog 10 | from Panda3DNodeEditor.NodeEditor import NodeEditor 11 | 12 | loadPrcFileData( 13 | "", 14 | """ 15 | sync-video #t 16 | textures-power-2 none 17 | window-title Node Editor 18 | maximized #t 19 | win-size 1280 720 20 | """) 21 | 22 | setupLog("NodeEditor") 23 | 24 | base = ShowBase() 25 | 26 | def set_dirty_name(): 27 | wp = WindowProperties() 28 | wp.setTitle("*Node Editor") 29 | base.win.requestProperties(wp) 30 | 31 | def set_clean_name(): 32 | wp = WindowProperties() 33 | wp.setTitle("Node Editor") 34 | base.win.requestProperties(wp) 35 | 36 | base.accept("request_dirty_name", set_dirty_name) 37 | base.accept("request_clean_name", set_clean_name) 38 | 39 | 40 | # Disable the default camera movements 41 | base.disableMouse() 42 | 43 | # 44 | # VIEW SETTINGS 45 | # 46 | base.win.setClearColor((0.16, 0.16, 0.16, 1)) 47 | render.setAntialias(AntialiasAttrib.MAuto) 48 | render2d.setAntialias(AntialiasAttrib.MAuto) 49 | 50 | NodeEditor(base.pixel2d) 51 | 52 | base.run() 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2020-2022, Fireclaw 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /Panda3DNodeEditor/NodeCore/Nodes/TestOutNode.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | __author__ = "Fireclaw the Fox" 4 | __license__ = """ 5 | Simplified BSD (BSD 2-Clause) License. 6 | See License.txt or http://opensource.org/licenses/BSD-2-Clause for more info 7 | """ 8 | 9 | from direct.gui import DirectGuiGlobals as DGG 10 | from direct.gui.DirectFrame import DirectFrame 11 | from panda3d.core import ( 12 | Point3, 13 | LPoint3, 14 | LVecBase3, 15 | LVecBase4, 16 | TextNode, 17 | Vec3, 18 | ) 19 | 20 | from DirectGuiExtension import DirectGuiHelper as DGH 21 | 22 | from Panda3DNodeEditor.NodeCore.Nodes.NodeBase import NodeBase 23 | from Panda3DNodeEditor.NodeCore.Sockets.InSocket import InSocket 24 | 25 | class Node(NodeBase): 26 | def __init__(self, parent): 27 | NodeBase.__init__(self, "OUT", parent) 28 | self.addIn("In 1", InSocket) 29 | 30 | def logic(self): 31 | """Simply write the value in the nodes textfield""" 32 | if self.inputList[0].value is None: 33 | self.inputList[0].text["text"] = "In 1" 34 | self.inputList[0].text.resetFrameSize() 35 | self.inputList[0].resize(1) 36 | self.update() 37 | return 38 | self.inputList[0].text["text"] = str(self.inputList[0].getValue()) 39 | self.inputList[0].text["frameSize"] = None 40 | self.inputList[0].text.resetFrameSize() 41 | 42 | newSize = max(1, DGH.getRealWidth(self.inputList[0].text) + 0.2) 43 | self.inputList[0].resize(newSize) 44 | 45 | self.update() 46 | -------------------------------------------------------------------------------- /Panda3DNodeEditor/NodeCore/Sockets/InSocket.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # This file was created using the DirectGUI Designer 5 | 6 | from Panda3DNodeEditor.NodeCore.Sockets.SocketBase import SocketBase, INSOCKET 7 | 8 | from direct.gui.DirectFrame import DirectFrame 9 | from direct.gui.DirectLabel import DirectLabel 10 | from panda3d.core import TextNode 11 | 12 | class InSocket(SocketBase): 13 | def __init__(self, node, name): 14 | SocketBase.__init__(self, node, name) 15 | 16 | self.type = INSOCKET 17 | 18 | self.frame = DirectFrame( 19 | frameColor=(0.25, 0.25, 0.25, 1), 20 | frameSize=(-1, 0, -self.height, 0), 21 | parent=node.frame, 22 | ) 23 | 24 | SocketBase.createPlug(self, self.frame) 25 | 26 | self.text = DirectLabel( 27 | frameColor=(0, 0, 0, 0), 28 | frameSize=(0, 1, -self.height, 0), 29 | scale=(1, 1, 1), 30 | text=self.name, 31 | text_align=TextNode.A_left, 32 | text_scale=(0.1, 0.1), 33 | text_pos=(0.1, -0.02), 34 | text_fg=(1, 1, 1, 1), 35 | text_bg=(0, 0, 0, 0), 36 | parent=self.frame, 37 | ) 38 | 39 | self.resize(1) 40 | 41 | def show(self, z, left): 42 | self.frame.setZ(z) 43 | self.frame.setX(left) 44 | 45 | def resize(self, newWidth): 46 | self.frame["frameSize"] = (0, newWidth, -self.height/2, self.height/2) 47 | self.text["frameSize"] = (0, newWidth, -self.height/2, self.height/2) 48 | -------------------------------------------------------------------------------- /Panda3DNodeEditor/NodeCore/Sockets/OutSocket.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # This file was created using the DirectGUI Designer 5 | 6 | from Panda3DNodeEditor.NodeCore.Sockets.SocketBase import SocketBase, OUTSOCKET 7 | 8 | from direct.gui.DirectFrame import DirectFrame 9 | from direct.gui.DirectLabel import DirectLabel 10 | from panda3d.core import TextNode 11 | 12 | class OutSocket(SocketBase): 13 | def __init__(self, node, name): 14 | SocketBase.__init__(self, node, name) 15 | 16 | self.type = OUTSOCKET 17 | 18 | self.frame = DirectFrame( 19 | frameColor=(0.25, 0.25, 0.25, 1), 20 | frameSize=(-1, 0, -self.height, 0), 21 | parent=node.frame, 22 | ) 23 | 24 | SocketBase.createPlug(self, self.frame) 25 | 26 | self.text = DirectLabel( 27 | frameColor=(0, 0, 0, 0), 28 | frameSize=(-1, 0, -self.height, 0), 29 | scale=(1, 1, 1), 30 | text=self.name, 31 | text_align=TextNode.A_right, 32 | text_scale=(0.1, 0.1), 33 | text_pos=(-0.1, -0.02), 34 | text_fg=(1, 1, 1, 1), 35 | text_bg=(0, 0, 0, 0), 36 | parent=self.frame, 37 | ) 38 | 39 | self.resize(1) 40 | 41 | def show(self, z, right): 42 | self.frame.setZ(z) 43 | self.frame.setX(right) 44 | 45 | def resize(self, newWidth): 46 | self.frame["frameSize"] = (-newWidth, 0, -self.height/2, self.height/2) 47 | self.text["frameSize"] = (-newWidth, 0, -self.height/2, self.height/2) 48 | -------------------------------------------------------------------------------- /Panda3DNodeEditor/GUI/MainView.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from direct.gui import DirectGuiGlobals as DGG 4 | from direct.gui.DirectFrame import DirectFrame 5 | 6 | from DirectGuiExtension.DirectBoxSizer import DirectBoxSizer 7 | from DirectGuiExtension.DirectAutoSizer import DirectAutoSizer 8 | 9 | from Panda3DNodeEditor.GUI.MenuBar import MenuBar 10 | 11 | 12 | class MainView(): 13 | def __init__(self, parent, customNodeMap, customExporterMap): 14 | logging.debug("Setup GUI") 15 | 16 | self.menuBarHeight = 24 17 | 18 | # 19 | # LAYOUT SETUP 20 | # 21 | 22 | # the box everything get's added to 23 | self.main_box = DirectBoxSizer( 24 | frameColor=(0,0,0,0), 25 | state=DGG.DISABLED, 26 | orientation=DGG.VERTICAL, 27 | autoUpdateFrameSize=False) 28 | # our root element for the main box 29 | self.main_sizer = DirectAutoSizer( 30 | frameColor=(0,0,0,0), 31 | parent=parent, 32 | child=self.main_box, 33 | childUpdateSizeFunc=self.main_box.refresh 34 | ) 35 | 36 | # our menu bar 37 | self.menu_bar_sizer = DirectAutoSizer( 38 | updateOnWindowResize=False, 39 | frameColor=(0,0,0,0), 40 | parent=self.main_box, 41 | extendVertical=False) 42 | 43 | # CONNECT THE UI ELEMENTS 44 | self.main_box.addItem( 45 | self.menu_bar_sizer, 46 | updateFunc=self.menu_bar_sizer.refresh, 47 | skipRefresh=True) 48 | 49 | # 50 | # CONTENT SETUP 51 | # 52 | self.menu_bar = MenuBar(customNodeMap, customExporterMap) 53 | self.menu_bar_sizer.setChild(self.menu_bar.menu_bar) 54 | self.menu_bar_sizer["childUpdateSizeFunc"] = self.menu_bar.menu_bar.refresh 55 | 56 | self.main_box.refresh() 57 | -------------------------------------------------------------------------------- /Panda3DNodeEditor/NodeCore/NodeConnector.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | __author__ = "Fireclaw the Fox" 4 | __license__ = """ 5 | Simplified BSD (BSD 2-Clause) License. 6 | See License.txt or http://opensource.org/licenses/BSD-2-Clause for more info 7 | """ 8 | from uuid import uuid4 9 | from direct.showbase import ShowBaseGlobal 10 | from direct.directtools.DirectGeometry import LineNodePath 11 | 12 | class NodeConnector: 13 | def __init__(self, socketA, socketB): 14 | self.connectorID = uuid4() 15 | self.socketA = socketA 16 | self.socketB = socketB 17 | self.line = LineNodePath(ShowBaseGlobal.aspect2d, thickness=2, colorVec=(0.8,0.8,0.8,1)) 18 | self.draw() 19 | 20 | self.show = self.line.show 21 | self.hide = self.line.hide 22 | 23 | def update(self): 24 | self.line.reset() 25 | self.draw() 26 | 27 | def draw(self): 28 | self.line.moveTo(self.socketA.plug.getPos(ShowBaseGlobal.aspect2d)) 29 | self.line.drawTo(self.socketB.plug.getPos(ShowBaseGlobal.aspect2d)) 30 | self.line.create() 31 | 32 | def has(self, socket): 33 | """Returns True if one of the sockets this connector connects is 34 | the given socket""" 35 | return socket == self.socketA or socket == self.socketB 36 | 37 | def connects(self, a, b): 38 | """Returns True if this connector connects socket a and b""" 39 | return (a == self.socketA or a == self.socketB) and (b == self.socketA or b == self.socketB) 40 | 41 | def disconnect(self): 42 | self.line.reset() 43 | self.socketA.setConnected(False) 44 | self.socketB.setConnected(False) 45 | 46 | def setChecked(self): 47 | self.line.setColor(0,1,0,1) 48 | self.update() 49 | 50 | def setError(self, hasError): 51 | self.line.setColor(1,0,0,1) 52 | self.update() 53 | 54 | def __str__(self): 55 | return f"Connection {self.socketA.name} to {self.socketB.name}" 56 | -------------------------------------------------------------------------------- /Panda3DNodeEditor/SaveScripts/SaveJSON.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | __author__ = "Fireclaw the Fox" 4 | __license__ = """ 5 | Simplified BSD (BSD 2-Clause) License. 6 | See License.txt or http://opensource.org/licenses/BSD-2-Clause for more info 7 | """ 8 | 9 | import os 10 | import json 11 | import logging 12 | import tempfile 13 | 14 | from direct.gui import DirectGuiGlobals as DGG 15 | from direct.gui.DirectFrame import DirectFrame 16 | from direct.gui.DirectDialog import YesNoDialog 17 | 18 | from DirectFolderBrowser.DirectFolderBrowser import DirectFolderBrowser 19 | from Panda3DNodeEditor.Tools.JSONTools import JSONTools 20 | 21 | class Save: 22 | def __init__(self, nodes, connections, exceptionSave=False, filepath=None): 23 | self.jsonElements = JSONTools().get(nodes, connections) 24 | 25 | if exceptionSave: 26 | tmpPath = os.path.join(tempfile.gettempdir(), "NEExceptionSave.logic") 27 | self.__executeSave(tmpPath) 28 | logging.info("Wrote crash session file to {}".format(tmpPath)) 29 | else: 30 | if filepath is None: 31 | self.browser = DirectFolderBrowser( 32 | command=self.save, 33 | fileBrowser=True, 34 | askForOverwrite=True, 35 | defaultFilename="project.logic", 36 | title="Save Node Editor Project") 37 | else: 38 | self.__executeSave(filepath) 39 | 40 | def save(self, doSave): 41 | if doSave: 42 | path = self.browser.get() 43 | path = os.path.expanduser(path) 44 | path = os.path.expandvars(path) 45 | self.__executeSave(path) 46 | base.messenger.send("setLastPath", [path]) 47 | self.browser.destroy() 48 | del self.browser 49 | 50 | def __executeSave(self, path): 51 | with open(path, 'w') as outfile: 52 | json.dump(self.jsonElements, outfile, indent=2) 53 | 54 | base.messenger.send("NodeEditor_set_clean") 55 | -------------------------------------------------------------------------------- /Panda3DNodeEditor/Tools/JSONTools.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | __author__ = "Fireclaw the Fox" 4 | __license__ = """ 5 | Simplified BSD (BSD 2-Clause) License. 6 | See License.txt or http://opensource.org/licenses/BSD-2-Clause for more info 7 | """ 8 | class JSONTools: 9 | def get(self, nodes, connections): 10 | jsonElements = {} 11 | jsonElements["ProjectVersion"] = "0.1" 12 | jsonElements["Nodes"] = [] 13 | jsonElements["Connections"] = [] 14 | 15 | for node in nodes: 16 | jsonElements["Nodes"].append( 17 | { 18 | "id":str(node.nodeID), 19 | "type":node.__module__, 20 | "pos":str(node.frame.getPos()), 21 | "inSockets":self.__getSockets(node.inputList), 22 | "outSockets":self.__getSockets(node.outputList) 23 | } 24 | ) 25 | 26 | for connector in connections: 27 | jsonElements["Connections"].append( 28 | { 29 | "id":str(connector.connectorID), 30 | "nodeA_ID":str(connector.socketA.node.nodeID), 31 | "nodeB_ID":str(connector.socketB.node.nodeID), 32 | "socketA_ID":str(connector.socketA.socketID), 33 | "socketB_ID":str(connector.socketB.socketID), 34 | } 35 | ) 36 | return jsonElements 37 | 38 | def __getSockets(self, socketList): 39 | sockets = [] 40 | for socket in socketList: 41 | if not socket.connected: 42 | # only store values entered by the user, not by other 43 | # sockets as they should be recalculated on load 44 | sockets.append({ 45 | "id":str(socket.socketID), 46 | "value":str(socket.getValue()) 47 | }) 48 | else: 49 | sockets.append({ 50 | "id":str(socket.socketID), 51 | }) 52 | return sockets 53 | 54 | -------------------------------------------------------------------------------- /editorLogHandler.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import logging 4 | from datetime import datetime 5 | from logging.handlers import TimedRotatingFileHandler 6 | from logging import StreamHandler 7 | 8 | 9 | from panda3d.core import ( 10 | loadPrcFileData, 11 | loadPrcFile, 12 | Filename, 13 | ConfigVariableSearchPath, 14 | ) 15 | 16 | def setupLog(editor_name): 17 | # check if we have a config file 18 | home = os.path.expanduser("~") 19 | basePath = os.path.join(home, f".{editor_name}") 20 | if not os.path.exists(basePath): 21 | os.makedirs(basePath) 22 | logPath = os.path.join(basePath, "logs") 23 | if not os.path.exists(logPath): 24 | os.makedirs(logPath) 25 | 26 | # Remove log files older than 30 days 27 | for f in os.listdir(logPath): 28 | fParts = f.split(".") 29 | fDate = datetime.now() 30 | try: 31 | fDate = datetime.strptime(fParts[-1], "%Y-%m-%d_%H") 32 | delta = datetime.now() - fDate 33 | if delta.days > 30: 34 | #print(f"remove {os.path.join(logPath, f)}") 35 | os.remove(os.path.join(logPath, f)) 36 | except Exception: 37 | # this file does not have a date ending 38 | pass 39 | 40 | log_file = os.path.join(logPath, f"{editor_name}.log") 41 | handler = TimedRotatingFileHandler(log_file) 42 | consoleHandler = StreamHandler() 43 | logging.basicConfig( 44 | level=logging.DEBUG, 45 | handlers=[handler])#, consoleHandler]) 46 | config_file = os.path.join(basePath, f".{editor_name}.prc") 47 | if os.path.exists(config_file): 48 | loadPrcFile(Filename.fromOsSpecific(config_file)) 49 | 50 | # make sure to load our custom paths 51 | paths_cfg = ConfigVariableSearchPath("custom-model-path", "").getValue() 52 | for path in paths_cfg.getDirectories(): 53 | line = "model-path {}".format(str(path)) 54 | loadPrcFileData("", line) 55 | else: 56 | with open(config_file, "w") as prcFile: 57 | prcFile.write("skip-ask-for-quit #f\n") 58 | prcFile.write("create-executable-scripts #f\n") 59 | prcFile.write("show-toolbar #t\n") 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node Editor 2 | A generic Node Editor 3 | 4 | ## Features 5 | - Easily extensible 6 | - Edit nodes similar to blenders node editor 7 | 8 | ## Screenshots 9 | 10 | ![Editor Window](/Screenshots/NodeEditor1.png?raw=true "The Editor") 11 | 12 | ## Requirements 13 | - Python 3.x 14 | - Panda3D 1.10.4.1+ 15 | 16 | ## Manual 17 | Add nodes by selecting the node from the Nodes menu 18 | 19 | ### Startup 20 | To start the Node Editor, simply run the NodeEditor.py script 21 | 22 | python NodeEditor.py 23 | 24 | ### Basic Editing 25 | Adding Nodes 26 | 1. Select a Node from the menub 27 | 2. The node will be attached to your mouse, drag it wherever you want and left click to place it 28 | 29 | To connecting Nodes simply click and drag from a nodes output socket to an input socket of another nodepath. The direction you drag doesn't matter here, you can also drag from out- to input socket. 30 | To disconnect a connection between two sockets, simply repeat the same behaviour as when you want to connect them. 31 | 32 | Selecting nodes can either be done by left clicking them or by draging a frame around them. Use shift-left click to add nodes to the selection. Right- or left-click anywhere on the editor to deselect the nodes. 33 | 34 | Use the left mouse button and drag on any free space on the editor to move the editor area around 35 | 36 | ### Zooming 37 | Use the mousewheel or the view menu to zoom in and out in the editor. 38 | 39 | ### Copying Nodes 40 | Select one or more nodes and hit shift-D to copy all nodes and their connections. Drag them to the desired location and left click with the left mouse button to place them. 41 | 42 | ### Remove elements 43 | Click X while having at least one node selected or use the Tools menu. 44 | 45 | ### Save and loading 46 | To save and load a node setup, click on the File menu and select Save or Load and select a JSON file to store or load from. You may name the files however you want. 47 | 48 | ### Custom Nodes 49 | To add your own Nodes, create a new python script in the /NodeCore/Nodes folder. These Nodes need to derive from NodeBase and should at least implement a logic method that handles the in and output of the node. 50 | 51 | ## Known Bugs and missing features 52 | - Some more basic nodes 53 | - Configurations 54 | -------------------------------------------------------------------------------- /Panda3DNodeEditor/NodeCore/Sockets/BoolSocket.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # This file was created using the DirectGUI Designer 5 | 6 | from Panda3DNodeEditor.NodeCore.Sockets.SocketBase import SocketBase, INSOCKET 7 | 8 | from direct.gui.DirectFrame import DirectFrame 9 | from direct.gui.DirectLabel import DirectLabel 10 | from direct.gui.DirectCheckButton import DirectCheckButton 11 | from direct.gui import DirectGuiGlobals as DGG 12 | from panda3d.core import TextNode 13 | 14 | class BoolSocket(SocketBase): 15 | def __init__(self, node, name): 16 | SocketBase.__init__(self, node, name) 17 | 18 | self.type = INSOCKET 19 | 20 | self.frame = DirectFrame( 21 | frameColor=(0.25, 0.25, 0.25, 1), 22 | frameSize=(-1, 0, -self.height, 0), 23 | parent=node.frame, 24 | ) 25 | 26 | SocketBase.createPlug(self, self.frame) 27 | 28 | ''' 29 | self.text = DirectLabel( 30 | frameColor=(0, 0, 0, 0), 31 | frameSize=(0, 1, -self.height, 0), 32 | scale=(1, 1, 1), 33 | text=self.name, 34 | text_align=TextNode.A_left, 35 | text_scale=(0.1, 0.1), 36 | text_pos=(0.1, -0.02), 37 | text_fg=(1, 1, 1, 1), 38 | text_bg=(0, 0, 0, 0), 39 | parent=self.frame, 40 | )''' 41 | 42 | self.checkbox = DirectCheckButton( 43 | text = name, 44 | pos=(0.5,0,0), 45 | scale=.1, 46 | command=self.updateConnectedNodes, 47 | parent=self.frame) 48 | 49 | self.resize(1) 50 | 51 | def setValue(self, value): 52 | self.checkbox["indicatorValue"] = value 53 | self.checkbox.setIndicatorValue() 54 | 55 | def getValue(self): 56 | return self.checkbox["indicatorValue"] 57 | 58 | def show(self, z, left): 59 | self.frame.setZ(z) 60 | self.frame.setX(left) 61 | 62 | def resize(self, newWidth): 63 | self.frame["frameSize"] = (0, newWidth, -self.height/2, self.height/2) 64 | #self.text["frameSize"] = (0, newWidth, -self.height/2, self.height/2) 65 | 66 | def setConnected(self, connected): 67 | if connected: 68 | self.checkbox["state"] = DGG.DISABLED 69 | else: 70 | self.checkbox["state"] = DGG.NORMAL 71 | self.connected = connected 72 | -------------------------------------------------------------------------------- /Panda3DNodeEditor/NodeCore/Sockets/OptionSelectSocket.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # This file was created using the DirectGUI Designer 5 | 6 | import logging 7 | 8 | from Panda3DNodeEditor.NodeCore.Sockets.SocketBase import SocketBase, INSOCKET 9 | 10 | from direct.gui.DirectFrame import DirectFrame 11 | from direct.gui.DirectLabel import DirectLabel 12 | from direct.gui.DirectOptionMenu import DirectOptionMenu 13 | from direct.gui import DirectGuiGlobals as DGG 14 | from panda3d.core import TextNode 15 | 16 | class OptionSelectSocket(SocketBase): 17 | def __init__(self, node, name, options): 18 | SocketBase.__init__(self, node, name) 19 | 20 | self.height = 0.21 21 | 22 | self.type = INSOCKET 23 | 24 | self.frame = DirectFrame( 25 | frameColor=(0.25, 0.25, 0.25, 1), 26 | frameSize=(-1, 0, -self.height, 0), 27 | parent=node.frame, 28 | ) 29 | 30 | SocketBase.createPlug(self, self.frame) 31 | 32 | self.text = DirectLabel( 33 | frameColor=(0, 0, 0, 0), 34 | frameSize=(0, 1, -self.height, 0), 35 | scale=(1, 1, 1), 36 | text=self.name, 37 | text_align=TextNode.A_left, 38 | text_scale=(0.1, 0.1), 39 | text_pos=(0.1, -0.02), 40 | text_fg=(1, 1, 1, 1), 41 | text_bg=(0, 0, 0, 0), 42 | parent=self.frame, 43 | ) 44 | 45 | self.optionsfield = DirectOptionMenu( 46 | pos=(0.5,0,-0.01), 47 | borderWidth=(0.1,0.1), 48 | items=options, 49 | parent=self.frame, 50 | command=self.updateConnectedNodes, 51 | state=DGG.DISABLED) 52 | self.optionsfield.setScale(0.1) 53 | 54 | self.resize(1.7) 55 | 56 | def disable(self): 57 | self.optionsfield["state"] = DGG.DISABLED 58 | 59 | def enable(self): 60 | if not self.connected: 61 | self.optionsfield["state"] = DGG.NORMAL 62 | 63 | def setValue(self, value): 64 | try: 65 | self.optionsfield.set(value) 66 | except: 67 | logging.error(f"couldn't set the value {value} for the option selection") 68 | return 69 | 70 | def getValue(self): 71 | return self.optionsfield.get() 72 | 73 | def show(self, z, left): 74 | self.frame.setZ(z) 75 | self.frame.setX(left) 76 | 77 | def resize(self, newWidth): 78 | self.frame["frameSize"] = (0, newWidth, -self.height/2, self.height/2) 79 | self.text["frameSize"] = (0, newWidth, -self.height/2, self.height/2) 80 | 81 | def setConnected(self, connected): 82 | if connected: 83 | self.optionsfield["state"] = DGG.DISABLED 84 | else: 85 | self.optionsfield["state"] = DGG.NORMAL 86 | self.connected = connected 87 | -------------------------------------------------------------------------------- /Panda3DNodeEditor/NodeCore/Sockets/SocketBase.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | __author__ = "Fireclaw the Fox" 4 | __license__ = """ 5 | Simplified BSD (BSD 2-Clause) License. 6 | See License.txt or http://opensource.org/licenses/BSD-2-Clause for more info 7 | """ 8 | 9 | from panda3d.core import TransparencyAttrib 10 | from uuid import uuid4 11 | 12 | from direct.gui.DirectFrame import DirectFrame 13 | from direct.gui import DirectGuiGlobals as DGG 14 | 15 | OUTSOCKET = 0 16 | INSOCKET = 1 17 | 18 | class SocketBase: 19 | def __init__(self, node, name): 20 | self.socketID = uuid4() 21 | self.node = node 22 | self.name = name 23 | self.height = 0.2 24 | self.type = None 25 | self.value = None 26 | self.connected = False 27 | self.frame = None 28 | self.allowMultiConnect = False 29 | 30 | def enable(self): 31 | """Enable any elements on the node""" 32 | pass 33 | 34 | def disable(self): 35 | """Disable any elements on the node that could possbily interfer with 36 | the mouse watcher and the drag/drop feature""" 37 | pass 38 | 39 | def getValue(self): 40 | """Returns a string serializable value stored in this node""" 41 | return self.value 42 | 43 | def setValue(self, value): 44 | self.value = value 45 | 46 | def createPlug(self, parent): 47 | self.plug = DirectFrame( 48 | state = DGG.NORMAL, 49 | image="icons/Plug.png", 50 | image_scale=.05, 51 | frameColor=(0, 0, 0, 0), 52 | frameSize=(-0.05, 0.05, -0.05, 0.05), 53 | parent=parent, 54 | ) 55 | self.plug.setTransparency(TransparencyAttrib.M_multisample) 56 | self.setupBind() 57 | 58 | def setupBind(self): 59 | self.plug.bind(DGG.B1PRESS, self.startPlug) 60 | self.plug.bind(DGG.B1RELEASE, self.releasePlug) 61 | self.plug.bind(DGG.ENTER, self.endPlug) 62 | 63 | def startPlug(self, event): 64 | base.messenger.send("startPlug", [self]) 65 | base.messenger.send("startLineDrawing", [self.plug.getPos(render2d)]) 66 | 67 | def endPlug(self, event): 68 | taskMgr.remove("delayedPlugRelease") 69 | base.messenger.send("endPlug", [self]) 70 | base.messenger.send("connectPlugs") 71 | 72 | def releasePlug(self, event): 73 | base.messenger.send("stopLineDrawing") 74 | taskMgr.doMethodLater(0.2, base.messenger.send, "delayedPlugRelease", extraArgs=["cancelPlug"]) 75 | 76 | def updateConnectedNodes(self, *args): 77 | base.messenger.send("updateConnectedNodes", [self.node]) 78 | 79 | def setConnected(self, connected): 80 | self.connected = connected 81 | if self.connected: 82 | self.plug["image"] = "icons/PlugConnectedGood.png" 83 | else: 84 | self.plug["image"] = "icons/Plug.png" 85 | -------------------------------------------------------------------------------- /Panda3DNodeEditor/NodeCore/Sockets/TextSocket.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # This file was created using the DirectGUI Designer 5 | 6 | from Panda3DNodeEditor.NodeCore.Sockets.SocketBase import SocketBase, INSOCKET 7 | 8 | from direct.gui.DirectFrame import DirectFrame 9 | from direct.gui.DirectLabel import DirectLabel 10 | from direct.gui.DirectEntry import DirectEntry 11 | from direct.gui import DirectGuiGlobals as DGG 12 | from panda3d.core import TextNode 13 | 14 | class TextSocket(SocketBase): 15 | def __init__(self, node, name): 16 | SocketBase.__init__(self, node, name) 17 | 18 | self.height = 0.21 19 | 20 | self.type = INSOCKET 21 | 22 | self.frame = DirectFrame( 23 | frameColor=(0.25, 0.25, 0.25, 1), 24 | frameSize=(-1, 0, -self.height, 0), 25 | parent=node.frame, 26 | ) 27 | 28 | SocketBase.createPlug(self, self.frame) 29 | 30 | self.text = DirectLabel( 31 | frameColor=(0, 0, 0, 0), 32 | frameSize=(0, 1, -self.height, 0), 33 | scale=(1, 1, 1), 34 | text=self.name, 35 | text_align=TextNode.A_left, 36 | text_scale=(0.1, 0.1), 37 | text_pos=(0.1, -0.02), 38 | text_fg=(1, 1, 1, 1), 39 | text_bg=(0, 0, 0, 0), 40 | parent=self.frame, 41 | ) 42 | 43 | self.textfield = DirectEntry( 44 | pos=(0.5,0,-0.01), 45 | borderWidth=(0.1,0.1), 46 | width=10, 47 | parent=self.frame, 48 | command=self.updateConnectedNodes, 49 | focusOutCommand=self.updateConnectedNodes, 50 | overflow=True, 51 | state=DGG.DISABLED) 52 | self.textfield.setScale(0.1) 53 | 54 | self.resize(1.7) 55 | 56 | def disable(self): 57 | self.textfield["state"] = DGG.DISABLED 58 | 59 | def enable(self): 60 | if not self.connected: 61 | self.textfield["state"] = DGG.NORMAL 62 | 63 | def setValue(self, value): 64 | textAsString = "" 65 | try: 66 | textAsString = str(value) 67 | except: 68 | logging.error("couldn't convert node input value to string") 69 | return 70 | self.textfield.enterText(textAsString) 71 | 72 | def getValue(self): 73 | return self.textfield.get() 74 | 75 | def show(self, z, left): 76 | self.frame.setZ(z) 77 | self.frame.setX(left) 78 | 79 | def resize(self, newWidth): 80 | self.frame["frameSize"] = (0, newWidth, -self.height/2, self.height/2) 81 | self.text["frameSize"] = (0, newWidth, -self.height/2, self.height/2) 82 | 83 | def setConnected(self, connected): 84 | if connected: 85 | self.textfield["state"] = DGG.DISABLED 86 | else: 87 | self.textfield["state"] = DGG.NORMAL 88 | self.connected = connected 89 | -------------------------------------------------------------------------------- /Panda3DNodeEditor/NodeCore/Sockets/NumericSocket.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # This file was created using the DirectGUI Designer 5 | 6 | from Panda3DNodeEditor.NodeCore.Sockets.SocketBase import SocketBase, INSOCKET 7 | 8 | from direct.gui.DirectFrame import DirectFrame 9 | from direct.gui.DirectLabel import DirectLabel 10 | from direct.gui import DirectGuiGlobals as DGG 11 | from panda3d.core import TextNode 12 | 13 | from DirectGuiExtension.DirectSpinBox import DirectSpinBox 14 | 15 | class NumericSocket(SocketBase): 16 | def __init__(self, node, name): 17 | SocketBase.__init__(self, node, name) 18 | 19 | self.type = INSOCKET 20 | 21 | self.frame = DirectFrame( 22 | frameColor=(0.25, 0.25, 0.25, 1), 23 | frameSize=(-1, 0, -self.height, 0), 24 | parent=node.frame, 25 | ) 26 | 27 | SocketBase.createPlug(self, self.frame) 28 | 29 | self.text = DirectLabel( 30 | frameColor=(0, 0, 0, 0), 31 | frameSize=(0, 1, -self.height, 0), 32 | scale=(1, 1, 1), 33 | text=self.name, 34 | text_align=TextNode.A_left, 35 | text_scale=(0.1, 0.1), 36 | text_pos=(0.1, -0.02), 37 | text_fg=(1, 1, 1, 1), 38 | text_bg=(0, 0, 0, 0), 39 | parent=self.frame, 40 | ) 41 | 42 | self.spinBox = DirectSpinBox( 43 | pos=(0.5,0,0), 44 | value=5, 45 | minValue=-100, 46 | maxValue=100, 47 | repeatdelay=0.125, 48 | buttonOrientation=DGG.HORIZONTAL, 49 | valueEntry_text_align=TextNode.ACenter, 50 | borderWidth=(0.1,0.1), 51 | parent=self.frame, 52 | incButtonCallback=self.updateConnectedNodes, 53 | decButtonCallback=self.updateConnectedNodes,) 54 | self.spinBox.setScale(0.1) 55 | 56 | self.resize(1) 57 | 58 | def setValue(self, value): 59 | self.spinBox.setValue(value) 60 | 61 | def getValue(self): 62 | return self.spinBox.getValue() 63 | 64 | def show(self, z, left): 65 | self.frame.setZ(z) 66 | self.frame.setX(left) 67 | 68 | def resize(self, newWidth): 69 | self.frame["frameSize"] = (0, newWidth, -self.height/2, self.height/2) 70 | self.text["frameSize"] = (0, newWidth, -self.height/2, self.height/2) 71 | 72 | def setConnected(self, connected): 73 | if connected: 74 | self.spinBox["state"] = DGG.DISABLED 75 | self.spinBox.incButton["state"] = DGG.DISABLED 76 | self.spinBox.decButton["state"] = DGG.DISABLED 77 | self.spinBox.valueEntry["state"] = DGG.DISABLED 78 | else: 79 | self.spinBox["state"] = DGG.NORMAL 80 | self.spinBox.incButton["state"] = DGG.NORMAL 81 | self.spinBox.decButton["state"] = DGG.NORMAL 82 | self.spinBox.valueEntry["state"] = DGG.NORMAL 83 | self.connected = connected 84 | -------------------------------------------------------------------------------- /Panda3DNodeEditor/LoadScripts/LoadJSON.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | __author__ = "Fireclaw the Fox" 4 | __license__ = """ 5 | Simplified BSD (BSD 2-Clause) License. 6 | See License.txt or http://opensource.org/licenses/BSD-2-Clause for more info 7 | """ 8 | 9 | import os 10 | import json 11 | import logging 12 | 13 | # NOTE: LPoint3f is required for loading the position from json using eval 14 | from panda3d.core import LPoint3f 15 | 16 | from DirectFolderBrowser.DirectFolderBrowser import DirectFolderBrowser 17 | from uuid import UUID 18 | 19 | class Load: 20 | def __init__(self, nodeMgr): 21 | self.nodeMgr = nodeMgr 22 | self.browser = DirectFolderBrowser(self.load, True, defaultFilename="project.logic") 23 | 24 | def load(self, doLoad): 25 | if doLoad: 26 | path = self.browser.get() 27 | path = os.path.expanduser(path) 28 | path = os.path.expandvars(path) 29 | 30 | self.__executeLoad(path) 31 | 32 | self.browser.destroy() 33 | del self.browser 34 | 35 | def __executeLoad(self, path): 36 | fileContent = None 37 | try: 38 | with open(path, 'r') as infile: 39 | fileContent = json.load(infile) 40 | except Exception as e: 41 | print("Couldn't load project file {}".format(path)) 42 | print(e) 43 | return 44 | 45 | if fileContent is None: 46 | print("Problems reading file: {}".format(infile)) 47 | return 48 | 49 | # 1. Create all nodes 50 | jsonNodes = fileContent["Nodes"] 51 | newNodes = [] 52 | hasUnknownNodes = False 53 | for jsonNode in jsonNodes: 54 | node = self.nodeMgr.createNode(jsonNode["type"]) 55 | if node is None: 56 | logging.error(f"Couldn't load node of type: {jsonNode['type']}") 57 | hasUnknownNodes = True 58 | continue 59 | node.nodeID = UUID(jsonNode["id"]) 60 | node.setPos(eval(jsonNode["pos"])) 61 | for i in range(len(jsonNode["inSockets"])): 62 | inSocket = jsonNode["inSockets"][i] 63 | node.inputList[i].socketID = UUID(inSocket["id"]) 64 | if "value" in inSocket: 65 | node.inputList[i].setValue(inSocket["value"]) 66 | for i in range(len(jsonNode["outSockets"])): 67 | outSocket = jsonNode["outSockets"][i] 68 | node.outputList[i].socketID = UUID(outSocket["id"]) 69 | node.show() 70 | newNodes.append(node) 71 | 72 | if hasUnknownNodes: 73 | logging.info("Some nodes could not be loaded. Make sure all node extensions are available.") 74 | 75 | # 2. Connect all nodes 76 | jsonConnections = fileContent["Connections"] 77 | for jsonConnection in jsonConnections: 78 | # we have a connection of one of the to be copied nodes 79 | nodeA = None 80 | nodeB = None 81 | 82 | for node in newNodes: 83 | if node.nodeID == UUID(jsonConnection["nodeA_ID"]): 84 | nodeA = node 85 | elif node.nodeID == UUID(jsonConnection["nodeB_ID"]): 86 | nodeB = node 87 | 88 | if nodeA is None or nodeB is None: 89 | logging.error(f"could not connect nodes: {nodeA} - {nodeB}") 90 | continue 91 | 92 | socketA = None 93 | socketB = None 94 | for socket in nodeA.inputList + nodeA.outputList + nodeB.inputList + nodeB.outputList: 95 | if socket.socketID == UUID(jsonConnection["socketA_ID"]): 96 | socketA = socket 97 | elif socket.socketID == UUID(jsonConnection["socketB_ID"]): 98 | socketB = socket 99 | 100 | self.nodeMgr.connectPlugs(socketA, socketB) 101 | 102 | # 3. Run logic from all leave nodes down to the end 103 | self.nodeMgr.updateAllLeaveNodes() 104 | 105 | base.messenger.send("NodeEditor_set_clean") 106 | 107 | base.messenger.send("setLastPath", [path]) 108 | 109 | -------------------------------------------------------------------------------- /Panda3DNodeEditor/GUI/MenuBar.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | __author__ = "Fireclaw the Fox" 4 | __license__ = """ 5 | Simplified BSD (BSD 2-Clause) License. 6 | See License.txt or http://opensource.org/licenses/BSD-2-Clause for more info 7 | """ 8 | 9 | from direct.gui import DirectGuiGlobals as DGG 10 | #DGG.BELOW = "below" 11 | 12 | from direct.gui.DirectFrame import DirectFrame 13 | from DirectGuiExtension.DirectMenuItem import DirectMenuItem, DirectMenuItemEntry, DirectMenuItemSubMenu, DirectMenuSeparator 14 | from DirectGuiExtension.DirectMenuBar import DirectMenuBar 15 | 16 | 17 | class MenuBar(): 18 | def __init__(self, customNodeMap, customExporterMap): 19 | screenWidthPx = base.getSize()[0] 20 | 21 | # 22 | # Menubar 23 | # 24 | self.menu_bar = DirectMenuBar( 25 | frameColor=(0.25, 0.25, 0.25, 1), 26 | frameSize=(0,screenWidthPx,-12, 12), 27 | autoUpdateFrameSize=False, 28 | pos=(0, 0, 0), 29 | itemMargin=(2,2,2,2), 30 | parent=base.pixel2d) 31 | 32 | customExporters = [] 33 | if len(customExporterMap) > 0: 34 | for entry, action in customExporterMap.items(): 35 | customExporters.append( 36 | DirectMenuItemEntry(entry, base.messenger.send, ["NodeEditor_customSave", [action]])) 37 | customExporters.append(DirectMenuSeparator()) 38 | 39 | self.file_entries = [ 40 | DirectMenuItemEntry("New", base.messenger.send, ["NodeEditor_new"]), 41 | DirectMenuSeparator(), 42 | DirectMenuItemEntry("Open", base.messenger.send, ["NodeEditor_load"]), 43 | DirectMenuItemEntry("Save", base.messenger.send, ["NodeEditor_save"]), 44 | DirectMenuItemEntry("Save As", base.messenger.send, ["NodeEditor_save_as"]), 45 | DirectMenuSeparator(), 46 | *customExporters, 47 | DirectMenuItemEntry("Quit", base.messenger.send, ["quit_app"]), 48 | ] 49 | self.file = self.__create_menu_item("File", self.file_entries) 50 | 51 | self.view_entries = [ 52 | DirectMenuItemEntry("Zoom In", base.messenger.send, ["NodeEditor_zoom", [True]]), 53 | DirectMenuItemEntry("Zoom Out", base.messenger.send, ["NodeEditor_zoom", [False]]), 54 | DirectMenuSeparator(), 55 | DirectMenuItemEntry("Reset Zoom", base.messenger.send, ["NodeEditor_zoom_reset"]), 56 | ] 57 | self.view = self.__create_menu_item("View", self.view_entries) 58 | 59 | self.tool_entries = [ 60 | DirectMenuItemEntry("Refresh", base.messenger.send, ["NodeEditor_refreshNodes"]), 61 | DirectMenuSeparator(), 62 | DirectMenuItemEntry("Copy Nodes", taskMgr.doMethodLater, [0.2, base.messenger.send, "delayedCopyFromMenu", ["NodeEditor_copyNodes"]]), 63 | DirectMenuItemEntry("Delete Nodes", base.messenger.send, ["NodeEditor_removeNode"]), 64 | ] 65 | self.tool = self.__create_menu_item("Tools", self.tool_entries) 66 | 67 | self.node_map = { 68 | "Math Nodes >":{ 69 | "Numeric Input":"NumericNode", 70 | "Addition":"AddNode", 71 | "Divide":"DivideNode", 72 | "Multiply":"MultiplyNode"}, 73 | "Boolean Nodes >":{ 74 | "Boolean Value":"BoolNode", 75 | "Boolean And":"BoolAnd", 76 | "Boolean Or":"BoolOr"}, 77 | "Simple Output":"TestOutNode" 78 | } 79 | self.node_map.update(customNodeMap) 80 | 81 | self.nodes_entries = [] 82 | for node_name, node in self.node_map.items(): 83 | if type(node) == str: 84 | self.nodes_entries.append( 85 | DirectMenuItemEntry( 86 | node_name, base.messenger.send, ["addNode", [node]])) 87 | elif type(node) == list: 88 | self.nodes_entries.append( 89 | DirectMenuItemEntry( 90 | node_name, base.messenger.send, ["addNode", [node[1]]])) 91 | elif type(node) == dict: 92 | sub_entries = [] 93 | for sub_node_name, sub_node in node.items(): 94 | if type(sub_node) == list: 95 | sub_entries.append( 96 | DirectMenuItemEntry( 97 | sub_node_name, 98 | base.messenger.send, 99 | ["addNode", [sub_node[1]]])) 100 | else: 101 | sub_entries.append( 102 | DirectMenuItemEntry( 103 | sub_node_name, 104 | base.messenger.send, 105 | ["addNode", [sub_node]])) 106 | self.nodes_entries.append( 107 | DirectMenuItemSubMenu( 108 | node_name, sub_entries)) 109 | 110 | self.nodes = self.__create_menu_item("Nodes", self.nodes_entries) 111 | 112 | self.menu_bar["menuItems"] = [self.file, self.view, self.tool, self.nodes] 113 | 114 | def __create_menu_item(self, text, entries): 115 | color = ( 116 | (0.25, 0.25, 0.25, 1), # Normal 117 | (0.35, 0.35, 1, 1), # Click 118 | (0.25, 0.25, 1, 1), # Hover 119 | (0.1, 0.1, 0.1, 1)) # Disabled 120 | 121 | sepColor = (0.7, 0.7, 0.7, 1) 122 | 123 | return DirectMenuItem( 124 | text=text, 125 | text_fg=(1,1,1,1), 126 | text_scale=0.8, 127 | items=entries, 128 | frameSize=(0,65/21,-7/21,17/21), 129 | frameColor=color, 130 | scale=21, 131 | relief=DGG.FLAT, 132 | item_text_fg=(1,1,1,1), 133 | item_text_scale=0.8, 134 | item_relief=DGG.FLAT, 135 | item_pad=(0.2, 0.2), 136 | itemFrameColor=color, 137 | separatorFrameColor=sepColor, 138 | popupMenu_frameColor=color, 139 | highlightColor=color[2]) 140 | -------------------------------------------------------------------------------- /Panda3DNodeEditor/icons/Plug.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 25 | 27 | 31 | 35 | 36 | 44 | 49 | 50 | 58 | 63 | 64 | 72 | 77 | 78 | 86 | 91 | 92 | 100 | 105 | 106 | 108 | 112 | 116 | 117 | 119 | 123 | 127 | 128 | 139 | 150 | 151 | 172 | 174 | 175 | 177 | image/svg+xml 178 | 180 | 181 | 182 | 183 | 184 | 189 | 196 | 197 | 201 | 211 | 212 | 213 | -------------------------------------------------------------------------------- /Panda3DNodeEditor/NodeCore/Nodes/NodeBase.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | __author__ = "Fireclaw the Fox" 4 | __license__ = """ 5 | Simplified BSD (BSD 2-Clause) License. 6 | See License.txt or http://opensource.org/licenses/BSD-2-Clause for more info 7 | """ 8 | from uuid import uuid4 9 | 10 | from direct.showbase.DirectObject import DirectObject 11 | from direct.gui import DirectGuiGlobals as DGG 12 | from direct.gui.DirectFrame import DirectFrame 13 | from panda3d.core import ( 14 | Point3, 15 | TextNode, 16 | Vec3, 17 | KeyboardButton, 18 | ) 19 | 20 | from DirectGuiExtension import DirectGuiHelper as DGH 21 | 22 | from Panda3DNodeEditor.NodeCore.Sockets.OutSocket import OutSocket 23 | 24 | class NodeBase(DirectObject): 25 | def __init__(self, name, parent): 26 | self.right = 0.5 27 | self.left = -0.5 28 | self.name = name 29 | self.nodeID = uuid4() 30 | self.inputList = [] 31 | self.outputList = [] 32 | self.selected = False 33 | self.allowRecursion = False 34 | self.hasError = False 35 | 36 | self.normalColor = (0.25, 0.25, 0.25, 1) 37 | self.highlightColor = (0.45, 0.45, 0.45, 1) 38 | self.errorColor = (1, 0.25, 0.25, 1) 39 | self.errorHighlightColor = (1, 0.45, 0.45, 1) 40 | 41 | self.frame = DirectFrame( 42 | state = DGG.NORMAL, 43 | text=name, 44 | text_align=TextNode.A_left, 45 | text_scale=0.1, 46 | text_pos=(self.left, 0.12), 47 | text_fg=(1,1,1,1), 48 | frameColor=self.normalColor, 49 | frameSize=(self.left, self.right, -.6, 0.2), 50 | parent=parent) 51 | 52 | self.setupBind() 53 | self.hide() 54 | 55 | self.setPos = self.frame.setPos 56 | self.getPos = self.frame.getPos 57 | 58 | def addIn(self, name, socketType, allowMultiConnect=False, extraArgs=None): 59 | """Add a new input socket of the given socket type""" 60 | if extraArgs is not None: 61 | inSocket = socketType(self, name, *extraArgs) 62 | else: 63 | inSocket = socketType(self, name) 64 | inSocket.allowMultiConnect = allowMultiConnect 65 | self.inputList.append(inSocket) 66 | 67 | def addOut(self, name): 68 | """Add a new output socket""" 69 | outSocket = OutSocket(self, name) 70 | self.outputList.append(outSocket) 71 | 72 | def isLeaveNode(self): 73 | """Returns true if this is a leave node. 74 | Leave nodes do not have any input connections. Either if no 75 | input sockets are defined at all or none of the sockets is 76 | connected.""" 77 | 78 | # check if we have any input sockets and if so if any of them is connected 79 | for inSocket in self.inputList: 80 | if inSocket.connected: return False 81 | return True 82 | 83 | def setError(self, hasError): 84 | self.hasError = hasError 85 | self.setColor() 86 | 87 | def logic(self): 88 | """Run the logic of this node, process all in and output data. 89 | This is a stub and should be overwritten by the derived classes""" 90 | pass 91 | 92 | def update(self): 93 | """Show all sockets and resize the frame to fit all sockets in""" 94 | z = 0 95 | 96 | fs = self.frame["frameSize"] 97 | maxWidth = fs[1] 98 | 99 | 100 | for socket in self.inputList + self.outputList: 101 | if socket.frame: 102 | maxWidth = max(maxWidth, DGH.getRealWidth(socket.frame)) 103 | self.left = -maxWidth / 2 104 | self.right = maxWidth / 2 105 | 106 | for outSocket in self.outputList: 107 | outSocket.resize(maxWidth) 108 | outSocket.show(z, self.right) 109 | z -= outSocket.height 110 | 111 | for inSocket in self.inputList: 112 | inSocket.resize(maxWidth) 113 | inSocket.show(z, self.left) 114 | z -= inSocket.height 115 | 116 | self.frame["frameSize"] = (self.left, self.right, z, fs[3]) 117 | self.frame["text_pos"] = (self.left, 0.12) 118 | 119 | base.messenger.send("NodeEditor_updateConnections") 120 | 121 | def create(self): 122 | """Place and show the node under the mouse and start draging it.""" 123 | mwn = base.mouseWatcherNode 124 | if mwn.hasMouse(): 125 | newPos = Point3(mwn.getMouse()[0], 0, mwn.getMouse()[1]) 126 | self.frame.setPos(render2d, newPos) 127 | self._dragStart(self.frame, None) 128 | self.show() 129 | 130 | def show(self): 131 | """Shows the Node frame and updates its sockets""" 132 | self.update() 133 | self.frame.show() 134 | 135 | def hide(self): 136 | """Hide the Node frame""" 137 | self.frame.hide() 138 | 139 | def destroy(self): 140 | self.frame.destroy() 141 | 142 | def enable(self): 143 | for socket in self.inputList + self.outputList: 144 | socket.enable() 145 | 146 | def disable(self): 147 | for socket in self.inputList + self.outputList: 148 | socket.disable() 149 | 150 | def setupBind(self): 151 | """Setup the mousebutton actions for drag and drop feature""" 152 | self.frame.bind(DGG.B1PRESS, self._dragStart, [self.frame]) 153 | self.frame.bind(DGG.B1RELEASE, self._dragStop) 154 | 155 | def select(self, select): 156 | """Set this node as selected or deselected""" 157 | if self.selected == select: return 158 | self.selected = select 159 | self.setColor() 160 | 161 | def setColor(self): 162 | if self.selected and not self.hasError: 163 | self.frame["frameColor"] = self.highlightColor 164 | elif self.selected and self.hasError: 165 | self.frame["frameColor"] = self.errorHighlightColor 166 | elif self.hasError: 167 | self.frame["frameColor"] = self.errorColor 168 | else: 169 | self.frame["frameColor"] = self.normalColor 170 | 171 | def _dragStart(self, nodeFrame, event): 172 | # Mark this node as selected 173 | base.messenger.send("selectNode", [self, True, base.mouseWatcherNode.isButtonDown(KeyboardButton.shift()), True]) 174 | # tell everyone we started to drag this node 175 | base.messenger.send("dragNodeStart", [self]) 176 | 177 | # Remove any previous started drag tasks 178 | taskMgr.remove("dragNodeDropTask") 179 | 180 | # get some positions 181 | vWidget2render2d = nodeFrame.getPos(render2d) 182 | vMouse2render2d = Point3(0) 183 | if event is not None: 184 | # we get the mouse position from the event 185 | vMouse2render2d = Point3(event.getMouse()[0], 0, event.getMouse()[1]) 186 | else: 187 | # we try to get the current mouse position from the mouse watcher 188 | mwn = base.mouseWatcherNode 189 | if mwn.hasMouse(): 190 | vMouse2render2d = Point3(mwn.getMouse()[0], 0, mwn.getMouse()[1]) 191 | editVec = Vec3(vWidget2render2d - vMouse2render2d) 192 | self.hasMoved = False 193 | 194 | # Initiate the task to move the node and pass it some initial values 195 | t = taskMgr.add(self.dragTask, "dragNodeDropTask") 196 | t.nodeFrame = nodeFrame 197 | t.editVec = editVec 198 | t.mouseVec = vMouse2render2d 199 | 200 | def dragTask(self, t): 201 | mwn = base.mouseWatcherNode 202 | if mwn.hasMouse(): 203 | # get the current mouse position fitting for a render2d position 204 | vMouse2render2d = Point3(mwn.getMouse()[0], 0, mwn.getMouse()[1]) 205 | 206 | # check if the cursor has moved enough to drag this node 207 | # this gives us some puffer zone for clicking 208 | if not self.hasMoved and (t.mouseVec - vMouse2render2d).length() < 0.01: return t.cont 209 | 210 | # We actually have moved now 211 | self.hasMoved = True 212 | 213 | # calculate the new position 214 | newPos = vMouse2render2d + t.editVec 215 | 216 | # move the node to the new position 217 | t.nodeFrame.setPos(render2d, newPos) 218 | 219 | # tell everyone we moved the node 220 | base.messenger.send("dragNodeMove", [t.mouseVec, vMouse2render2d]) 221 | 222 | return t.cont 223 | 224 | def _dragStop(self, event=None): 225 | self.ignore("mouse1-up") 226 | # remove the node dragging task 227 | taskMgr.remove("dragNodeDropTask") 228 | 229 | # check if the node has moved 230 | if not self.hasMoved: 231 | # we want to select this node as it has not been moved 232 | base.messenger.send("selectNode", [self, True, base.mouseWatcherNode.isButtonDown(KeyboardButton.shift())]) 233 | # tell everyone we stopped moving the node 234 | base.messenger.send("dragNodeStop", [self]) 235 | base.messenger.send("NodeEditor_set_dirty") 236 | 237 | def getLeftEdge(self): 238 | """Get the left edge of the frame as seen from the frame""" 239 | return self.frame["frameSize"][0] 240 | 241 | def getRightEdge(self): 242 | """Get the right edge of the frame as seen from the frame""" 243 | return self.frame["frameSize"][1] 244 | 245 | def getBottomEdge(self): 246 | """Get the bottom edge of the frame as seen from the frame""" 247 | return self.frame["frameSize"][2] 248 | 249 | def getTopEdge(self): 250 | """Get the top edge of the frame as seen from the frame""" 251 | return self.frame["frameSize"][3] 252 | 253 | def getLeft(self, np=None): 254 | """Get left edge of the frame with respect to it's position as seen from the given np""" 255 | if np is None: 256 | np = render2d 257 | return self.getPos(np).getX() + self.frame["frameSize"][0] 258 | 259 | def getRight(self, np=None): 260 | """Get right edge of the frame with respect to it's position as seen from the given np""" 261 | if np is None: 262 | np = render2d 263 | return self.getPos(np).getX() + self.frame["frameSize"][1] 264 | 265 | def getBottom(self, np=None): 266 | """Get bottom edge of the frame with respect to it's position as seen from the given np""" 267 | if np is None: 268 | np = render2d 269 | return self.getPos(np).getZ() + self.frame["frameSize"][2] 270 | 271 | def getTop(self, np=None): 272 | """Get top edge of the frame with respect to it's position as seen from the given np""" 273 | if np is None: 274 | np = render2d 275 | return self.getPos(np).getZ() + self.frame["frameSize"][3] 276 | -------------------------------------------------------------------------------- /Panda3DNodeEditor/NodeCore/NodeManager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | __author__ = "Fireclaw the Fox" 4 | __license__ = """ 5 | Simplified BSD (BSD 2-Clause) License. 6 | See License.txt or http://opensource.org/licenses/BSD-2-Clause for more info 7 | """ 8 | 9 | import logging 10 | 11 | import Panda3DNodeEditor 12 | from Panda3DNodeEditor import NodeCore 13 | from Panda3DNodeEditor.NodeCore.Nodes import * 14 | from Panda3DNodeEditor.NodeCore.Nodes.NodeBase import NodeBase 15 | from Panda3DNodeEditor.NodeCore.Sockets.SocketBase import OUTSOCKET, INSOCKET 16 | from Panda3DNodeEditor.NodeCore.NodeConnector import NodeConnector 17 | 18 | class NodeManager: 19 | def __init__(self, nodeViewNP=None, customNodeMap=None): 20 | # Node Management 21 | self.nodeList = [] 22 | 23 | # Socket connection Management 24 | self.connections = [] 25 | self.startSocket = None 26 | self.endSocket = None 27 | 28 | # Drag and Drop feature 29 | self.selectedNodes = [] 30 | 31 | self.nodeViewNP = nodeViewNP 32 | 33 | self.customNodeMap = customNodeMap 34 | 35 | def cleanup(self): 36 | self.deselectAll() 37 | self.removeAllNodes() 38 | 39 | self.startSocket = None 40 | self.endSocket = None 41 | 42 | self.nodeList = [] 43 | self.connections = [] 44 | self.selectedNodes = [] 45 | 46 | base.messenger.send("NodeEditor_set_clean") 47 | 48 | #------------------------------------------------------------------- 49 | # NODE MANAGEMENT 50 | #------------------------------------------------------------------- 51 | def getAllNodes(self): 52 | return self.nodeList 53 | 54 | def createNode(self, nodeType): 55 | """Creates a node of the given type and returns it. Returns None 56 | if the node could not be created. 57 | nodeType can be either a node class type or a string representing such a type""" 58 | if isinstance(nodeType, str): 59 | try: 60 | logging.debug(f"try load node from string: {nodeType}.Node") 61 | nodeType = eval(nodeType + ".Node") 62 | except: 63 | logging.debug("loading custom node") 64 | nodeClassName = nodeType.split(".")[-1] 65 | # try for the custom nodes 66 | for entry, customNodeType in self.customNodeMap.items(): 67 | if type(customNodeType) == dict: 68 | for sub_entry, sub_customNodeType in customNodeType.items(): 69 | if sub_customNodeType[0] == nodeClassName: 70 | nodeType = sub_customNodeType[1] 71 | else: 72 | if customNodeType[0] == nodeClassName: 73 | nodeType = customNodeType[1] 74 | if nodeType is None: 75 | logging.error(f"couldn't add unknown node type: {nodeType}") 76 | else: 77 | logging.debug(f"found Node type: {nodeType}") 78 | try: 79 | node = nodeType(self.nodeViewNP) 80 | self.nodeList.append(node) 81 | base.messenger.send("NodeEditor_set_dirty") 82 | return node 83 | except Exception as e: 84 | logging.error("Failed to load node type", exc_info=True) 85 | return None 86 | 87 | def addNode(self, nodeType): 88 | """Create a node of the given type""" 89 | self.deselectAll() 90 | node = None 91 | if isinstance(nodeType, str): 92 | node = eval(nodeType + ".Node")(self.nodeViewNP) 93 | else: 94 | node = nodeType(self.nodeViewNP) 95 | node.create() 96 | self.nodeList.append(node) 97 | base.messenger.send("NodeEditor_set_dirty") 98 | return node 99 | 100 | def removeNode(self, selectedNodes=[]): 101 | """Remove all selected nodes""" 102 | if selectedNodes == []: 103 | selectedNodes = self.selectedNodes 104 | for node in selectedNodes: 105 | for connector in self.connections[:]: 106 | if connector.socketA.node is node or connector.socketB.node is node: 107 | connector.disconnect() 108 | self.connections.remove(connector) 109 | # Update logic of the disconnected existing socket node 110 | if connector.socketA.node is node: 111 | self.updateSocketNodeLogic(connector.socketB) 112 | else: 113 | self.updateSocketNodeLogic(connector.socketA) 114 | self.nodeList.remove(node) 115 | node.destroy() 116 | del node 117 | 118 | def removeAllNodes(self): 119 | """Remove all nodes and connections that are currently in the editor""" 120 | 121 | # Remove all connections 122 | for connector in self.connections[:]: 123 | connector.disconnect() 124 | self.connections.remove(connector) 125 | 126 | # Remove all nodes 127 | for node in self.nodeList[:]: 128 | self.nodeList.remove(node) 129 | node.destroy() 130 | del node 131 | 132 | base.messenger.send("NodeEditor_set_clean") 133 | 134 | def selectNode(self, node, selected, addToSelection=False, deselectOthersIfUnselected=False): 135 | """Select or deselect the given node according to the boolean value in selected. 136 | If addToSelection is set, other nodes currently selected are not deselected. 137 | If deselectOthersIfUnselected is set, other nodes will be deselected if the given node 138 | is not yet selected""" 139 | # check if we want to add to the current selection 140 | if not addToSelection: 141 | if deselectOthersIfUnselected: 142 | if not node.selected: 143 | self.deselectAll(node) 144 | else: 145 | self.deselectAll(node) 146 | 147 | # check if we want to select or deselect the node 148 | if selected: 149 | # Select 150 | # Only select if it's not already selected 151 | if node not in self.selectedNodes: 152 | node.select(True) 153 | self.selectedNodes.append(node) 154 | else: 155 | # Deselect 156 | node.select(False) 157 | self.selectedNodes.remove(node) 158 | 159 | def deselectAll(self, excludedNode=None): 160 | """Deselect all nodes""" 161 | for node in self.nodeList: 162 | if node is excludedNode: continue 163 | node.select(False) 164 | self.selectedNodes = [] 165 | 166 | def copyNodes(self): 167 | """Copy all selected nodes (light copies) and start dragging 168 | with the mouse cursor""" 169 | 170 | if self.selectedNodes == []: return 171 | 172 | # a mapping of old to new nodes 173 | nodeMapping = {} 174 | socketMapping = {} 175 | 176 | # create shallow copies of all nodes 177 | newNodeList = [] 178 | for node in self.selectedNodes: 179 | newNode = type(node)(self.nodeViewNP) 180 | newNode.show() 181 | newNode.frame.setPos(node.frame.getPos()) 182 | newNodeList.append(newNode) 183 | self.nodeList.append(newNode) 184 | nodeMapping[node] = newNode 185 | for i in range(len(node.inputList)): 186 | socketMapping[node.inputList[i]] = newNode.inputList[i] 187 | for i in range(len(node.outputList)): 188 | socketMapping[node.outputList[i]] = newNode.outputList[i] 189 | 190 | # get connections of to be copied nodes 191 | for connector in self.connections: 192 | if connector.socketA.node in self.selectedNodes and connector.socketB.node in self.selectedNodes: 193 | # we have a connection of one of the to be copied nodes 194 | newNodeA = nodeMapping[connector.socketA.node] 195 | newNodeB = nodeMapping[connector.socketB.node] 196 | 197 | newSocketA = socketMapping[connector.socketA] 198 | newSocketB = socketMapping[connector.socketB] 199 | 200 | self.connectPlugs() 201 | connector = NodeConnector(newSocketA, newSocketB) 202 | self.connections.append(connector) 203 | newSocketA.setConnected(True) 204 | newSocketB.setConnected(True) 205 | 206 | # deselect all nodes 207 | self.deselectAll() 208 | 209 | # now only select the newly created ones 210 | for node in newNodeList: 211 | node.select(True) 212 | self.selectedNodes.append(node) 213 | 214 | # start the dragging of the new nodes 215 | dragNode = newNodeList[0] 216 | dragNode.accept("mouse1-up", dragNode._dragStop) 217 | dragNode._dragStart(dragNode.frame, None) 218 | 219 | self.updateAllLeaveNodes() 220 | 221 | base.messenger.send("NodeEditor_set_dirty") 222 | 223 | #------------------------------------------------------------------- 224 | # CONNECTION MANAGEMENT 225 | #------------------------------------------------------------------- 226 | def setStartPlug(self, socket): 227 | """Set the start socket for a possible connection""" 228 | self.startSocket = socket 229 | 230 | def setEndPlug(self, socket): 231 | """Set the end socket for a possible connection""" 232 | self.endSocket = socket 233 | 234 | def cancelPlug(self): 235 | """A possible connection between two sockets has been canceled""" 236 | self.startSocket = None 237 | self.endSocket = None 238 | 239 | def connectPlugs(self, startSocket=None, endSocket=None): 240 | """Create a line connection between the sockets set in 241 | self.startSocket and self.endSocket if a connection is possible 242 | 243 | This function will not allow a connection with only one socket 244 | set, if both sockets are of the same type or on the same node.""" 245 | 246 | if startSocket is not None: 247 | self.startSocket = startSocket 248 | if endSocket is not None: 249 | self.endSocket = endSocket 250 | 251 | # only do something if we actually have two sockets 252 | if self.startSocket is None or self.endSocket is None: 253 | return 254 | 255 | # check if the "IN" socket has no connections otherwise we can't connect 256 | if (self.startSocket.type == INSOCKET and self.startSocket.connected) \ 257 | or (self.endSocket.type == INSOCKET and self.endSocket.connected): 258 | # check if this is our connection. If so, we want to disconnect 259 | for connector in self.connections[:]: 260 | if connector.connects(self.startSocket, self.endSocket): 261 | connector.disconnect() 262 | self.connections.remove(connector) 263 | 264 | # Update logic of the sockets' nodes 265 | self.updateDisconnectedNodesLogic(self.startSocket, self.endSocket) 266 | 267 | self.startSocket = None 268 | self.endSocket = None 269 | base.messenger.send("NodeEditor_set_dirty") 270 | return 271 | if (self.startSocket.type == INSOCKET and not self.startSocket.allowMultiConnect) \ 272 | or (self.endSocket.type == INSOCKET and not self.endSocket.allowMultiConnect): 273 | return 274 | 275 | # check if the nodes and types are different, we can't connect 276 | # a node with itself or an "OUT" type with another "OUT" type. 277 | # The same applies to "IN" type sockets 278 | if self.startSocket.node is not self.endSocket.node \ 279 | and self.startSocket.type != self.endSocket.type: 280 | connector = NodeConnector(self.startSocket, self.endSocket) 281 | self.connections.append(connector) 282 | self.startSocket.setConnected(True) 283 | self.endSocket.setConnected(True) 284 | outSocketNode = self.startSocket.node if self.startSocket.type is OUTSOCKET else self.endSocket.node 285 | self.updateConnectedNodes(outSocketNode) 286 | self.startSocket = None 287 | self.endSocket = None 288 | base.messenger.send("NodeEditor_set_dirty") 289 | return connector 290 | 291 | def showConnections(self): 292 | for connector in self.connections: 293 | connector.show() 294 | 295 | def hideConnections(self): 296 | for connector in self.connections: 297 | connector.hide() 298 | 299 | def updateAllLeaveNodes(self): 300 | leaves = [] 301 | for node in self.nodeList: 302 | if node.isLeaveNode(): 303 | leaves.append(node) 304 | 305 | for leave in leaves: 306 | leave.logic() 307 | self.updateConnectedNodes(leave) 308 | 309 | def updateDisconnectedNodesLogic(self, socketA, socketB): 310 | """ 311 | Updates the logic of the nodes of socket A and socket B. 312 | The respective input plug type sockets value will be set to None. 313 | """ 314 | # Update logic of out socket node 315 | outSocketNode = socketA.node if socketA.type is OUTSOCKET else socketB.node 316 | outSocketNode.logic() 317 | self.updateConnectedNodes(outSocketNode) 318 | 319 | # Update logic of in socket node 320 | inSocketNode = socketA.node if socketA.type is INSOCKET else socketB.node 321 | inSocket = socketA if socketA.type is INSOCKET else socketB 322 | inSocket.value = None 323 | inSocketNode.logic() 324 | self.updateConnectedNodes(inSocketNode) 325 | 326 | def updateSocketNodeLogic(self, socket): 327 | """Update the logic of the given node and all nodes connected 328 | down the given""" 329 | if socket.type is INSOCKET: 330 | socket.value = None 331 | socket.node.logic() 332 | self.updateConnectedNodes(socket.node) 333 | 334 | def updateConnectedNodes(self, leaveNode): 335 | self.processedConnections = [] 336 | self.startNode = leaveNode 337 | self.__updateConnectedNodes(leaveNode) 338 | 339 | def __updateConnectedNodes(self, leaveNode): 340 | """Update logic of all nodes connected the leave nodes 341 | out sockets recursively down to the last connected node.""" 342 | for connector in self.connections: 343 | for outSocket in leaveNode.outputList: 344 | outSock = None 345 | inSock = None 346 | 347 | if connector.socketA is outSocket: 348 | inSock = connector.socketB 349 | outSock = connector.socketA 350 | elif connector.socketB is outSocket: 351 | inSock = connector.socketA 352 | outSock = connector.socketB 353 | else: 354 | continue 355 | 356 | connector.setChecked() 357 | outSock.node.logic() 358 | inSock.setValue(outSock.getValue()) 359 | inSock.node.logic() 360 | 361 | if connector in self.processedConnections: 362 | # this connector is leading to a recursion 363 | connector.setError(True) 364 | continue 365 | self.processedConnections.append(connector) 366 | 367 | self.__updateConnectedNodes(inSock.node) 368 | 369 | self.processedConnections.remove(connector) 370 | 371 | def updateConnections(self, args=None): 372 | """Update line positions of all connections""" 373 | for connector in self.connections: 374 | connector.update() 375 | -------------------------------------------------------------------------------- /Panda3DNodeEditor/NodeEditor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | __author__ = "Fireclaw the Fox" 4 | __license__ = """ 5 | Simplified BSD (BSD 2-Clause) License. 6 | See License.txt or http://opensource.org/licenses/BSD-2-Clause for more info 7 | """ 8 | import os 9 | from panda3d.core import ( 10 | LPoint2f, 11 | Point2, 12 | Point3, 13 | CardMaker, 14 | Vec3, 15 | TransparencyAttrib, 16 | loadPrcFileData, 17 | Filename) 18 | 19 | # We need showbase to make this script directly runnable 20 | from direct.showbase.DirectObject import DirectObject 21 | from direct.directtools.DirectGeometry import LineNodePath 22 | 23 | from Panda3DNodeEditor.SaveScripts.SaveJSON import Save 24 | from Panda3DNodeEditor.LoadScripts.LoadJSON import Load 25 | from Panda3DNodeEditor.GUI.MainView import MainView 26 | from Panda3DNodeEditor.NodeCore.NodeManager import NodeManager 27 | 28 | class NodeEditor(DirectObject): 29 | def __init__(self, parent, customNodeMap={}, customExporterMap={}): 30 | 31 | DirectObject.__init__(self) 32 | 33 | fn = Filename.fromOsSpecific(os.path.dirname(__file__)) 34 | fn.makeTrueCase() 35 | self.icon_dir = str(fn) + "/" 36 | loadPrcFileData("", f"model-path {self.icon_dir}") 37 | 38 | # 39 | # PROJECT RELATED 40 | # 41 | self.lastSavePath = None 42 | self.dirty = False 43 | 44 | # 45 | # NODE VIEW 46 | # 47 | self.viewNP = aspect2d.attachNewNode("viewNP") 48 | self.viewNP.setScale(0.5) 49 | 50 | # 51 | # NODE MANAGER 52 | # 53 | self.nodeMgr = NodeManager(self.viewNP, customNodeMap) 54 | 55 | # Drag view 56 | self.mouseSpeed = 1 57 | self.mousePos = None 58 | self.startCameraMovement = False 59 | 60 | # Box select 61 | # variables to store the start and current pos of the mousepointer 62 | self.startPos = LPoint2f(0,0) 63 | self.lastPos = LPoint2f(0,0) 64 | # variables for the to be drawn box 65 | self.boxCardMaker = CardMaker("SelectionBox") 66 | self.boxCardMaker.setColor(1,1,1,0.25) 67 | self.box = None 68 | 69 | # 70 | # MENU BAR 71 | # 72 | self.mainView = MainView(parent, customNodeMap, customExporterMap) 73 | 74 | self.enable_editor() 75 | 76 | # ------------------------------------------------------------------ 77 | # FRAME COMPATIBILITY FUNCTIONS 78 | # ------------------------------------------------------------------ 79 | def is_dirty(self): 80 | """ 81 | This method returns True if an unsaved state of the editor is given 82 | """ 83 | return self.dirty 84 | 85 | def enable_editor(self): 86 | """ 87 | Enable the editor. 88 | """ 89 | self.enable_events() 90 | 91 | # Task for handling dragging of the camera/view 92 | taskMgr.add(self.updateCam, "NodeEditor_task_camActualisation", priority=-4) 93 | 94 | self.viewNP.show() 95 | self.nodeMgr.showConnections() 96 | 97 | def disable_editor(self): 98 | """ 99 | Disable the editor. 100 | """ 101 | self.ignore_all() 102 | taskMgr.remove("NodeEditor_task_camActualisation") 103 | 104 | self.viewNP.hide() 105 | self.nodeMgr.hideConnections() 106 | 107 | def do_exception_save(self): 108 | """ 109 | Save content of editor if the application crashes 110 | """ 111 | Save(self.nodeMgr.nodeList, self.nodeMgr.connections, True) 112 | 113 | # ------------------------------------------------------------------ 114 | # NODE EDITOR RELATED EVENTS 115 | # ------------------------------------------------------------------ 116 | def enable_events(self): 117 | # Add nodes 118 | self.accept("addNode", self.nodeMgr.addNode) 119 | # Remove nodes 120 | self.accept("NodeEditor_removeNode", self.nodeMgr.removeNode) 121 | self.accept("x", self.nodeMgr.removeNode) 122 | self.accept("delete", self.nodeMgr.removeNode) 123 | # Selecting 124 | self.accept("selectNode", self.nodeMgr.selectNode) 125 | # Deselecting 126 | self.accept("mouse3", self.nodeMgr.deselectAll) 127 | # Node Drag and Drop 128 | self.accept("dragNodeStart", self.setDraggedNode) 129 | self.accept("dragNodeMove", self.updateNodeMove) 130 | self.accept("dragNodeStop", self.updateNodeStop) 131 | # Duplicate/Copy nodes 132 | self.accept("shift-d", self.nodeMgr.copyNodes) 133 | self.accept("NodeEditor_copyNodes", self.nodeMgr.copyNodes) 134 | # Refresh node logics 135 | self.accept("ctlr-r", self.nodeMgr.updateAllLeaveNodes) 136 | self.accept("NodeEditor_refreshNodes", self.nodeMgr.updateAllLeaveNodes) 137 | 138 | # 139 | # SOCKET RELATED EVENTS 140 | # 141 | self.accept("updateConnectedNodes", self.nodeMgr.updateConnectedNodes) 142 | # Socket connection with drag and drop 143 | self.accept("startPlug", self.nodeMgr.setStartPlug) 144 | self.accept("endPlug", self.nodeMgr.setEndPlug) 145 | self.accept("connectPlugs", self.nodeMgr.connectPlugs) 146 | self.accept("cancelPlug", self.nodeMgr.cancelPlug) 147 | # Draw line while connecting sockets 148 | self.accept("startLineDrawing", self.startLineDrawing) 149 | self.accept("stopLineDrawing", self.stopLineDrawing) 150 | 151 | # 152 | # CONNECTION RELATED EVENTS 153 | # 154 | self.accept("NodeEditor_updateConnections", self.nodeMgr.updateConnections) 155 | 156 | # 157 | # PROJECT MANAGEMENT 158 | # 159 | self.accept("NodeEditor_new", self.newProject) 160 | self.accept("NodeEditor_save", self.saveProject) 161 | self.accept("NodeEditor_save_as", self.saveAsProject) 162 | self.accept("NodeEditor_load", self.loadProject) 163 | self.accept("quit_app", exit) 164 | 165 | self.accept("NodeEditor_set_dirty", self.set_dirty) 166 | self.accept("NodeEditor_set_clean", self.set_clean) 167 | 168 | self.accept("setLastPath", self.setLastPath) 169 | 170 | # 171 | # EXPORTERS 172 | # 173 | self.accept("NodeEditor_customSave", self.customExport) 174 | 175 | # 176 | # EDITOR VIEW 177 | # 178 | # Zooming 179 | self.accept("NodeEditor_zoom", self.zoom) 180 | self.accept("NodeEditor_zoom_reset", self.zoomReset) 181 | self.accept("wheel_up", self.zoom, [True]) 182 | self.accept("wheel_down", self.zoom, [False]) 183 | 184 | # Drag view 185 | self.accept("mouse2", self.setMoveCamera, [True]) 186 | self.accept("mouse2-up", self.setMoveCamera, [False]) 187 | 188 | # Box select 189 | # accept the 1st mouse button events to start and stop the draw 190 | self.accept("mouse1", self.startBoxDraw) 191 | self.accept("mouse1-up", self.stopBoxDraw) 192 | 193 | # ------------------------------------------------------------------ 194 | # PROJECT FUNCTIONS 195 | # ------------------------------------------------------------------ 196 | def newProject(self): 197 | self.nodeMgr.cleanup() 198 | self.lastSavePath = None 199 | 200 | def saveProject(self): 201 | Save(self.nodeMgr.nodeList, self.nodeMgr.connections, filepath=self.lastSavePath) 202 | 203 | def saveAsProject(self): 204 | Save(self.nodeMgr.nodeList, self.nodeMgr.connections) 205 | 206 | def loadProject(self): 207 | self.nodeMgr.cleanup() 208 | Load(self.nodeMgr) 209 | 210 | def customExport(self, exporter): 211 | exporter(self.nodeMgr.nodeList, self.nodeMgr.connections) 212 | 213 | def setLastPath(self, path): 214 | self.lastSavePath = path 215 | 216 | def set_dirty(self): 217 | base.messenger.send("request_dirty_name") 218 | self.dirty = True 219 | 220 | def set_clean(self): 221 | base.messenger.send("request_clean_name") 222 | self.dirty = False 223 | self.hasSaved = True 224 | 225 | # ------------------------------------------------------------------ 226 | # CAMERA SPECIFIC FUNCTIONS 227 | # ------------------------------------------------------------------ 228 | def setMoveCamera(self, moveCamera): 229 | """Start dragging around the editor area/camera""" 230 | # store the mouse position if weh have a mouse 231 | if base.mouseWatcherNode.hasMouse(): 232 | x = base.mouseWatcherNode.getMouseX() 233 | y = base.mouseWatcherNode.getMouseY() 234 | self.mousePos = Point2(x, y) 235 | # set the variable according to if we want to move the camera or not 236 | self.startCameraMovement = moveCamera 237 | 238 | def updateCam(self, task): 239 | """Task that will move the editor area/camera around according 240 | to mouse movements""" 241 | # variables to store the mouses current x and y position 242 | x = 0.0 243 | y = 0.0 244 | if base.mouseWatcherNode.hasMouse(): 245 | # get the mouse position 246 | x = base.mouseWatcherNode.getMouseX() 247 | y = base.mouseWatcherNode.getMouseY() 248 | if base.mouseWatcherNode.hasMouse() \ 249 | and self.mousePos is not None \ 250 | and self.startCameraMovement: 251 | # Move the viewer node aspect independent 252 | wp = base.win.getProperties() 253 | aspX = 1.0 254 | aspY = 1.0 255 | wpXSize = wp.getXSize() 256 | wpYSize = wp.getYSize() 257 | if wpXSize > wpYSize: 258 | aspX = wpXSize / float(wpYSize) 259 | else: 260 | aspY = wpYSize / float(wpXSize) 261 | mouseMoveX = (self.mousePos.getX() - x) / self.viewNP.getScale().getX() * self.mouseSpeed * aspX 262 | mouseMoveY = (self.mousePos.getY() - y) / self.viewNP.getScale().getZ() * self.mouseSpeed * aspY 263 | self.mousePos = Point2(x, y) 264 | 265 | self.viewNP.setX(self.viewNP, -mouseMoveX) 266 | self.viewNP.setZ(self.viewNP, -mouseMoveY) 267 | 268 | self.nodeMgr.updateConnections() 269 | 270 | # continue the task until it got manually stopped 271 | return task.cont 272 | 273 | def zoom(self, zoomIn): 274 | """Zoom the editor in or out dependent on the value in zoomIn""" 275 | zoomFactor = 0.05 276 | maxZoomIn = 2 277 | maxZoomOut = 0.1 278 | if zoomIn: 279 | s = self.viewNP.getScale() 280 | if s.getX()-zoomFactor < maxZoomIn and s.getY()-zoomFactor < maxZoomIn and s.getZ()-zoomFactor < maxZoomIn: 281 | self.viewNP.setScale(s.getX()+zoomFactor,s.getY()+zoomFactor,s.getZ()+zoomFactor) 282 | else: 283 | s = self.viewNP.getScale() 284 | if s.getX()-zoomFactor > maxZoomOut and s.getY()-zoomFactor > maxZoomOut and s.getZ()-zoomFactor > maxZoomOut: 285 | self.viewNP.setScale(s.getX()-zoomFactor,s.getY()-zoomFactor,s.getZ()-zoomFactor) 286 | self.nodeMgr.updateConnections() 287 | 288 | def zoomReset(self): 289 | """Set the zoom level back to the default""" 290 | self.viewNP.setScale(0.5) 291 | self.nodeMgr.updateConnections() 292 | 293 | # ------------------------------------------------------------------ 294 | # DRAG LINE 295 | # ------------------------------------------------------------------ 296 | def startLineDrawing(self, startPos): 297 | """Start a task that will draw a line from the given start 298 | position to the cursor""" 299 | self.line = LineNodePath(render2d, thickness=2, colorVec=(0.8,0.8,0.8,1)) 300 | self.line.moveTo(startPos) 301 | t = taskMgr.add(self.drawLineTask, "drawLineTask") 302 | t.startPos = startPos 303 | 304 | def drawLineTask(self, task): 305 | """Draws a line from a given start position to the cursor""" 306 | mwn = base.mouseWatcherNode 307 | if mwn.hasMouse(): 308 | pos = Point3(mwn.getMouse()[0], 0, mwn.getMouse()[1]) 309 | 310 | self.line.reset() 311 | self.line.moveTo(task.startPos) 312 | self.line.drawTo(pos) 313 | self.line.create() 314 | return task.cont 315 | 316 | def stopLineDrawing(self): 317 | """Stop the task that draws a line to the cursor""" 318 | taskMgr.remove("drawLineTask") 319 | if self.line is not None: 320 | self.line.reset() 321 | self.line = None 322 | 323 | # ------------------------------------------------------------------ 324 | # EDITOR NODE DRAGGING UPDATE 325 | # ------------------------------------------------------------------ 326 | def setDraggedNode(self, node): 327 | """This will set the node that is currently dragged around 328 | as well as update other selected nodes which will be moved 329 | in addition to the main dragged node""" 330 | self.draggedNode = node 331 | self.draggedNode.disable() 332 | self.tempNodePositions = {} 333 | for node in self.nodeMgr.selectedNodes: 334 | self.tempNodePositions[node] = node.frame.getPos(render2d) 335 | 336 | def updateNodeMove(self, mouseA, mouseB): 337 | """Will be called as long as a node is beeing dragged around""" 338 | for node in self.nodeMgr.selectedNodes: 339 | if node is not self.draggedNode and node in self.tempNodePositions.keys(): 340 | editVec = Vec3(self.tempNodePositions[node] - mouseA) 341 | newPos = mouseB + editVec 342 | node.frame.setPos(render2d, newPos) 343 | self.nodeMgr.updateConnections() 344 | 345 | def updateNodeStop(self, node=None): 346 | """Will be called when a node dragging stopped""" 347 | if self.draggedNode is None: return 348 | self.draggedNode.enable() 349 | self.draggedNode = None 350 | self.tempNodePositions = {} 351 | self.nodeMgr.updateConnections() 352 | 353 | # ------------------------------------------------------------------ 354 | # SELECTION BOX 355 | # ------------------------------------------------------------------ 356 | def startBoxDraw(self): 357 | """Start drawing the box""" 358 | if base.mouseWatcherNode.hasMouse(): 359 | # get the mouse position 360 | self.startPos = LPoint2f(base.mouseWatcherNode.getMouse()) 361 | taskMgr.add(self.dragBoxDrawTask, "dragBoxDrawTask") 362 | 363 | def stopBoxDraw(self): 364 | """Stop the draw box task and remove the box""" 365 | if not taskMgr.hasTaskNamed("dragBoxDrawTask"): return 366 | taskMgr.remove("dragBoxDrawTask") 367 | if self.startPos is None or self.lastPos is None: return 368 | self.nodeMgr.deselectAll() 369 | 370 | if self.box is not None: 371 | for node in self.nodeMgr.getAllNodes(): 372 | # store some view scales for calculations 373 | viewXScale = self.viewNP.getScale().getX() 374 | viewZScale = self.viewNP.getScale().getZ() 375 | 376 | # calculate the node edges 377 | p = node.frame.get_parent() 378 | nodeLeft = node.getLeft(p) * viewXScale / base.a2dRight 379 | nodeRight = node.getRight(p) * viewXScale / base.a2dRight 380 | nodeBottom = node.getBottom(p) * viewZScale / base.a2dTop 381 | nodeTop = node.getTop(p) * viewZScale / base.a2dTop 382 | 383 | # calculate bounding box edges 384 | left = min(self.lastPos.getX(), self.startPos.getX()) 385 | right = max(self.lastPos.getX(), self.startPos.getX()) 386 | top = max(self.lastPos.getY(), self.startPos.getY()) 387 | bottom = min(self.lastPos.getY(), self.startPos.getY()) 388 | 389 | l_in_l = left > nodeLeft 390 | r_in_r = right < nodeRight 391 | b_in_t = bottom < nodeTop 392 | t_in_b = top > nodeBottom 393 | 394 | r_in_l = right > nodeLeft 395 | l_in_r = left < nodeRight 396 | t_in_t = top < nodeTop 397 | b_in_b = bottom > nodeBottom 398 | 399 | l_out_l = left < nodeLeft 400 | r_out_r = right > nodeRight 401 | b_out_b = bottom < nodeBottom 402 | t_out_t = top > nodeTop 403 | 404 | nodeHit = False 405 | 406 | # 407 | # Side checks 408 | # 409 | if l_in_l and r_in_r and t_in_b and t_in_t: 410 | # Box hits middle from below 411 | nodeHit = True 412 | elif l_in_l and r_in_r and b_in_t and b_in_b: 413 | # Box hits middle from above 414 | nodeHit = True 415 | elif t_in_t and b_in_b and r_in_l and r_in_r: 416 | # Box hits middle from left 417 | nodeHit = True 418 | elif t_in_t and b_in_b and l_in_r and l_in_l: 419 | # Box hits middle from right 420 | nodeHit = True 421 | 422 | # 423 | # Corner checks 424 | # 425 | elif r_in_l and r_in_r and b_in_t and b_in_b: 426 | # Box hits top left corner 427 | nodeHit = True 428 | elif l_in_r and l_in_l and b_in_t and b_in_b: 429 | # Box hits top right corner 430 | nodeHit = True 431 | elif l_in_r and l_in_l and t_in_b and t_in_t: 432 | # Box hits bottom right corner 433 | nodeHit = True 434 | elif r_in_l and r_in_r and t_in_b and t_in_t: 435 | # Box hits bottom left corner 436 | nodeHit = True 437 | 438 | # 439 | # surrounding checks 440 | # 441 | elif l_in_r and l_in_l and t_out_t and b_out_b: 442 | # box encases the left of the node 443 | nodeHit = True 444 | elif r_in_l and r_in_r and t_out_t and b_out_b: 445 | # box encases the right of the node 446 | nodeHit = True 447 | elif t_in_b and t_in_t and r_out_r and l_out_l: 448 | # box encases the bottom of the node 449 | nodeHit = True 450 | elif b_in_t and b_in_b and r_out_r and l_out_l: 451 | # box encases the top of the node 452 | nodeHit = True 453 | 454 | # 455 | # Node fully encased 456 | # 457 | elif l_out_l and r_out_r and b_out_b and t_out_t: 458 | # box encased fully 459 | nodeHit = True 460 | 461 | if nodeHit: 462 | self.nodeMgr.selectNode(node, True, True) 463 | 464 | # Cleanup the selection box 465 | self.box.removeNode() 466 | self.startPos = None 467 | self.lastPos = None 468 | 469 | def dragBoxDrawTask(self, task): 470 | """This task will track the mouse position and actualize the box's size 471 | according to the first click position of the mouse""" 472 | if base.mouseWatcherNode.hasMouse(): 473 | if self.startPos is None: 474 | self.startPos = LPoint2f(base.mouseWatcherNode.getMouse()) 475 | # get the current mouse position 476 | self.lastPos = LPoint2f(base.mouseWatcherNode.getMouse()) 477 | else: 478 | return task.cont 479 | 480 | # check if we already have a box 481 | if self.box != None: 482 | # if so, remove that old box 483 | self.box.removeNode() 484 | # set the box's size 485 | self.boxCardMaker.setFrame( 486 | self.lastPos.getX(), 487 | self.startPos.getX(), 488 | self.startPos.getY(), 489 | self.lastPos.getY()) 490 | # generate, setup and draw the box 491 | node = self.boxCardMaker.generate() 492 | self.box = render2d.attachNewNode(node) 493 | self.box.setBin("gui-popup", 25) 494 | self.box.setTransparency(TransparencyAttrib.M_alpha) 495 | 496 | # run until the task is manually stopped 497 | return task.cont 498 | --------------------------------------------------------------------------------