├── 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 | 
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 |
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 |
--------------------------------------------------------------------------------