Visual Programming with Dynamo atop Freecad API?

Have some feature requests, feedback, cool stuff to share, or want to know where FreeCAD is going? This is the place.
Forum rules
Be nice to others! Read the FreeCAD code of conduct!
looo
Posts: 2917
Joined: Mon Nov 11, 2013 5:29 pm

Re: Visual Programming with Dynamo atop Freecad API?

Postby looo » Fri Aug 29, 2014 11:31 am

I've played with the blender nodesystem some time ago, and really liked it. So I have done a short FreeCADmacro, that paints widgets into a QGraphicsScene. It doesen't look very nice but a modern look could maybe achievd with some qt knowledge ;)
functionality: right click = drag the node, left click = connect radio buttons)

Code: Select all

from __future__ import division
import sys
from PySide import QtGui, QtCore
import math

import FreeCAD as App



class QAnchorNode(QtGui.QRadioButton):
    """a RadioButton which has some connection info and connections to other AnchorNodes
            make new connection with left click"""
    def __init__(self, parent=None, scene=None):
        super(QAnchorNode, self).__init__(parent)
        self.scene = scene
        self.line = []
        self.connection = False
        self.connection_node = None
        scene.node_list.append(self)

    @property
    def global_pos(self):
        return self.parent().mapToGlobal(self.pos())

    def mousePressEvent(self, event):
        pos = self.global_pos
        line = QtCore.QLineF(pos.x(), pos.y(), pos.x(), pos.y())
        line_item = QtGui.QGraphicsLineItem(line, scene=self.scene)
        line_item.setZValue(0)
        self.line.append(line_item)

    def mouseMoveEvent(self, event):
        if event.buttons() == QtCore.Qt.LeftButton:
            if len(self.line) > 0:
                self.connection = True
                pos = self.mapToGlobal(event.pos())
                for node in self.scene.node_list:
                    node_pos = node.global_pos
                    if PointNorm(node_pos, pos) < 30 and node is not self:
                        self.connection_node = node
                        pos = node_pos
                        break
                    else:
                        self.connection_node = None
                line = self.line[-1]
                l = line.line()
                x1 = l.x1()
                y1 = l.y1()
                line.setLine(x1, y1, pos.x(), pos.y())


    def mouseReleaseEvent(self, event):
        if self.connection:
            if self.connection_node is not None:
                line = QNodeLine(self, self.connection_node, scene=self.scene)
                self.connection_node.line.append(line)
                self.line[-1] = line
                self.setChecked(1)
                self.connection_node.setChecked(1)
                self.connection_node = None
            else: #(mouserelease not distinguish between right or left click)
                self.line.pop(-1)
            self.connection = False


class QNodeWidget(QtGui.QGraphicsProxyWidget):
    """The QGraphicsProxyWidget allows to get the Widget on the scene"""
    def __init__(self, widget):
        super(QNodeWidget, self).__init__()
        self.setWidget(widget)
        self.current_pos = None

    def mousePressEvent(self, event):
        if event.buttons() == QtCore.Qt.RightButton:
            self.current_pos = event.pos()
        else:
            super(QNodeWidget, self).mousePressEvent(event)

    def mouseMoveEvent(self, event):
        if event.buttons() == QtCore.Qt.RightButton:
            self.setPos(self.mapToParent(event.pos()) - self.current_pos)
            for anchor_node in self.widget().children():
                if isinstance(anchor_node, QAnchorNode):
                    for line in anchor_node.line:
                        line.update_line()
        else:
            super(QNodeWidget, self).mouseMoveEvent(event)


class QNodeScene(QtGui.QGraphicsScene):
    """A GraphicsScene which has a list of all the nodes"""
    def __init__(self):
        super(QNodeScene, self).__init__()
        self.setSceneRect(0, 0, 1000, 1000)
        self.node_list=[]


class QNodeView(QtGui.QGraphicsView):
    def wheelEvent(self, event):
        scale_value = 1 + (event.delta() / 5000)
        self.scale(scale_value, scale_value)


class QNodeLine(QtGui.QGraphicsLineItem):
    """ a Line Element to connect Nodes"""
    def __init__(self, node1, node2, scene=None):
        super(QNodeLine, self).__init__(scene=scene)
        self.node1 = node1
        self.node2 = node2
        self.linef = QtCore.QLineF(node1.global_pos, node2.global_pos)
        self.setZValue(-1)
        self.setLine(self.linef)

    def update_line(self):
        self.linef.setP1(self.node1.global_pos)
        self.linef.setP2(self.node2.global_pos)
        self.setLine(self.linef)



def PointNorm(p1, p2):
    return(math.sqrt((p1.x() - p2.x()) ** 2 + (p1.y() - p2.y()) ** 2))


def getMainWindow():
    """ Return the FreeCAD main window. """
    toplevel = QtGui.qApp.topLevelWidgets()
    for i in toplevel:
        if i.metaObject().className() == "Gui::MainWindow":
            return i
    return None


def getMdiArea():
    """ Return FreeCAD MdiArea. """
    mw = getMainWindow()
    if not mw:
        return None
    childs = mw.children()
    for c in childs:
        if isinstance(c, QtGui.QMdiArea):
            return c
    return None



def test():
    App.newDocument()

def createNodeView(winTitle="NodeView"):
    mdi = getMdiArea()
    if not mdi:
        return None
    scene = QNodeScene()
    view = QNodeView()
    view.setScene(scene)
    view.setWindowTitle(winTitle)
    sub = mdi.addSubWindow(view)

    wid = QtGui.QWidget()
    layout = QtGui.QVBoxLayout(wid)
    layout.addWidget(QtGui.QLabel("text"))
    qp = QtGui.QPushButton("Create new Document")
    wid.connect(qp, QtCore.SIGNAL("clicked()"), test)
    layout.addWidget(qp)
    node1 = QAnchorNode(wid, scene)
    layout.addWidget(node1)

    wid2 = QtGui.QWidget()
    layout2 = QtGui.QVBoxLayout(wid2)
    layout2.addWidget(QtGui.QLabel("text"))
    node2 = QAnchorNode(wid2, scene)
    layout2.addWidget(node2)

    node_wid = QNodeWidget(wid)
    node_wid2 = QNodeWidget(wid2)

    scene.addItem(node_wid)
    scene.addItem(node_wid2)
    sub.show()

createNodeView()
So what do you think? Is qt a good tool for such a nodesystem?
please help with my conda-packaging efforts: https://liberapay.com/looooo/
User avatar
yorik
Site Admin
Posts: 11567
Joined: Tue Feb 17, 2009 9:16 pm
Location: São Paulo, Brazil
Contact:

Re: Visual Programming with Dynamo atop Freecad API?

Postby yorik » Fri Aug 29, 2014 2:14 pm

Ah excellent! it's even zoomable! Great experiment...
looo
Posts: 2917
Joined: Mon Nov 11, 2013 5:29 pm

Re: Visual Programming with Dynamo atop Freecad API?

Postby looo » Sat Aug 30, 2014 12:12 pm

A little update on that macro: added multiply-operator and add-operator ;)
if you want to try this: seperate the widgets first, they are all placed at the same location and then connect them and press calculate.

Code: Select all

from __future__ import division
import sys
from PySide import QtGui, QtCore
import math

import FreeCAD as App


class QAnchorNode(QtGui.QCheckBox):
    """a RadioButton which has some connection info and connections to other AnchorNodes
            make new connection with left click"""
    def __init__(self, parent=None, scene=None):
        super(QAnchorNode, self).__init__(parent)
        self.scene = scene
        self.line = []
        self.connection = False
        self.connection_node = None
        scene.node_list.append(self)

    @property
    def global_pos(self):
        temp = QtCore.QPointF(self.width() / 2, self.height() / 2)
        return QtCore.QPointF(self.parent().mapToGlobal(self.pos())) + temp

    def mousePressEvent(self, event):
        self.connection = True
        pos = self.global_pos
        line = QtCore.QLineF(pos.x(), pos.y(), pos.x(), pos.y())
        line_item = QtGui.QGraphicsLineItem(line, scene=self.scene)
        line_item.setZValue(0)
        self.line.append(line_item)

    def mouseMoveEvent(self, event):
        if event.buttons() == QtCore.Qt.LeftButton:
            if len(self.line) > 0:
                pos = self.mapToGlobal(event.pos())
                for node in self.scene.node_list:
                    node_pos = node.global_pos
                    if PointNorm(node_pos, pos) < 10 and node.parent() is not self.parent():
                        self.connection_node = node
                        pos = node_pos
                        break
                    else:
                        self.connection_node = None
                line = self.line[-1]
                l = line.line()
                x1 = l.x1()
                y1 = l.y1()
                line.setLine(x1, y1, pos.x(), pos.y())

    def mouseReleaseEvent(self, event):
        if self.connection:
            if self.connection_node is not None:
                line = QNodeLine(self, self.connection_node, scene=self.scene)
                self.connection_node.line.append(line)
                self.line[-1] = line
                self.setChecked(1)
                self.connection_node.setChecked(1)
                self.connection_node = None
            self.connection = False
        for i, p in enumerate(self.line):
            if not isinstance(p, QNodeLine):
                self.line.pop(i)

    def delete_line(self, line):
        node1 = line.node1
        node2 = line.node2
        node1.line.remove(line)
        node2.line.remove(line)
        if len(node1.line) == 0:
            node1.setChecked(0)
        if len(node2.line) == 0:
            node2.setChecked(0)



class QInputNode(QAnchorNode):
    """this node only accept one line"""
    def mousePressEvent(self, event):
        if len(self.line) == 0:
            super(QInputNode, self).mousePressEvent(event)
        else:
            self.delete_line(self.line[0])
            super(QInputNode, self).mousePressEvent(event)

    def mouseMoveEvent(self, event):
        super(QInputNode, self).mouseMoveEvent(event)
        if not isinstance(self.connection_node, QOutputNode):
            self.connection_node = None

    def get_connected_Node(self):
        if len(self.line) > 0:
            node1 = self.line[0].node1
            node2 = self.line[0].node2
            if node1 == self:
                return node2
            else:
                return node1
        return None


class QOutputNode(QAnchorNode):
    def mouseReleaseEvent(self, event):
        if self.connection:
            if self.connection_node is not None:
                if len(self.connection_node.line) != 0:
                    self.connection_node.delete_line(self.connection_node.line[0])
        super(QOutputNode, self).mouseReleaseEvent(event)

    def mouseMoveEvent(self, event):
        super(QOutputNode, self).mouseMoveEvent(event)
        if not isinstance(self.connection_node, QInputNode):
            self.connection_node = None


class QNodeProxyWidget(QtGui.QGraphicsProxyWidget):
    """The QGraphicsProxyWidget allows to get the Widget on the scene"""
    def __init__(self, widget=None, parent=None):
        super(QNodeProxyWidget, self).__init__(parent=parent)
        self.setWidget(widget)
        self.current_pos = None

    def mousePressEvent(self, event):
        if event.buttons() == QtCore.Qt.RightButton:
            self.current_pos = event.pos()
        else:
            super(QNodeProxyWidget, self).mousePressEvent(event)

    def mouseMoveEvent(self, event):
        super(QNodeProxyWidget, self).mouseMoveEvent(event)
        if event.buttons() == QtCore.Qt.RightButton:
            self.setPos(self.mapToParent(event.pos()) - self.current_pos)
            for i in range(2):
                for anchor_node in self.anchor_nodes:
                    for line in anchor_node.line:
                        line.update_line()

        else:
            super(QNodeProxyWidget, self).mouseMoveEvent(event)

    @property
    def anchor_nodes(self):
        anchor_nodes = []
        self.find_anchor_nodes(self.widget(), arr=anchor_nodes)
        return(anchor_nodes)

    @staticmethod
    def find_anchor_nodes(widget, arr=[]):
        for child in widget.children():
            if isinstance(child, QAnchorNode):
                arr.append(child)
            else:
                QNodeProxyWidget.find_anchor_nodes(child, arr=arr)


class QNodeScene(QtGui.QGraphicsScene):
    """A GraphicsScene which has a list of all the nodes"""
    def __init__(self):
        super(QNodeScene, self).__init__()
        self.setSceneRect(-100, -100, 1000, 1000)
        self.node_list=[]


class QNodeView(QtGui.QGraphicsView):
    def wheelEvent(self, event):
        scale_value = 1 + (event.delta() / 5000)
        self.scale(scale_value, scale_value)


class QNodeLine(QtGui.QGraphicsLineItem):
    """ a Line Element to connect Nodes"""
    def __init__(self, node1, node2, scene=None):
        super(QNodeLine, self).__init__(scene=scene)
        self.node1 = node1
        self.node2 = node2
        self.linef = QtCore.QLineF(node1.global_pos, node2.global_pos)
        self.setZValue(-1)
        self.setLine(self.linef)
        self.delete_line = False

    def update_line(self):
        self.linef.setP1(self.node1.global_pos)
        self.linef.setP2(self.node2.global_pos)
        self.setLine(self.linef)


def PointNorm(p1, p2):
    return(math.sqrt((p1.x() - p2.x()) ** 2 + (p1.y() - p2.y()) ** 2))


def getMainWindow():
    """ Return the FreeCAD main window. """
    toplevel = QtGui.qApp.topLevelWidgets()
    for i in toplevel:
        if i.metaObject().className() == "Gui::MainWindow":
            return i
    return None


def getMdiArea():
    """ Return FreeCAD MdiArea. """
    mw = getMainWindow()
    if not mw:
        return None
    childs = mw.children()
    for c in childs:
        if isinstance(c, QtGui.QMdiArea):
            return c
    return None


class QManipulatorNodeWidget(QtGui.QWidget):
    def __init__(self, widget=QtGui.QWidget(), inp=False, outp=False, scene=None, parent=None):
        super(QManipulatorNodeWidget, self).__init__(parent=parent)
        self.scene = scene
        self.layout = QtGui.QHBoxLayout(self)
        self.inp = None
        self.outp = None
        if inp:
            self.inp = QInputNode(self, self.scene)
            self.layout.addWidget(self.inp)
        else:
            self.layout.addWidget(QtGui.QWidget())
        self.base_widget = widget or QtGui.QWidget()
        self.base_widget.setParent(self)
        self.layout.addWidget(self.base_widget)
        if outp:
            self.outp = QOutputNode(self, self.scene)
            self.layout.addWidget(self.outp)
        else:
            self.layout.addWidget(QtGui.QWidget())
        self.output = self.fakefunc

    @property
    def input(self):
        """calls the output of connectet input"""
        if self.inp:
            connected_node = self.inp.get_connected_Node()
            if connected_node:
                connected_manipulator_wid = connected_node.parent()
                return connected_manipulator_wid.output()
        return None

    @staticmethod
    def fakefunc():
        return None


class SliderNode(QtGui.QWidget):
    def __init__(self, scene):
        super(SliderNode, self).__init__()
        self.layout = QtGui.QVBoxLayout(self)
        self.slider = QtGui.QSlider(QtCore.Qt.Horizontal)
        self.slider_manipulator = QManipulatorNodeWidget(widget=self.slider, outp=True,  scene=scene)
        self.value = QtGui.QLabel(str(self.slider.value()))
        self.value.setAlignment(QtCore.Qt.AlignCenter)
        self.layout.addWidget(self.slider_manipulator)
        self.layout.addWidget(self.value)
        self.connect(self.slider, QtCore.SIGNAL("valueChanged(int)"), self.update_value)
        self.add_to(scene)
        self.slider_manipulator.output = self.output

    def output(self):
        return self.slider.value()

    def update_value(self, val):
        self.value.setText(str(val))

    def add_to(self, scene):
        item = QNodeProxyWidget(self)
        scene.addItem(item)

class GetValueNode(QtGui.QWidget):
    def __init__(self, scene):
        super(GetValueNode, self).__init__()
        self.layout = QtGui.QVBoxLayout(self)
        self.val = QtGui.QLabel()
        self.inp = QManipulatorNodeWidget(widget=self.val, inp=True, scene=scene)
        self.press = QtGui.QPushButton("Calculate Value")
        self.layout.addWidget(self.val)
        self.layout.addWidget(self.inp)
        self.layout.addWidget(self.press)
        self.val.setAlignment(QtCore.Qt.AlignCenter)
        self.connect(self.press, QtCore.SIGNAL("clicked()"), self.set_value)
        self.add_to(scene)

    def add_to(self, scene):
        item = QNodeProxyWidget(self)
        scene.addItem(item)

    def set_value(self):
        self.val.setText(str(self.inp.input))



class AddNode(QtGui.QWidget):
    def __init__(self, scene):
        super(AddNode, self).__init__()
        self.layout = QtGui.QVBoxLayout(self)
        self.titel = QtGui.QLabel("Add")
        self.layout.addWidget(self.titel)
        self.titel.setAlignment(QtCore.Qt.AlignCenter)
        self.val1 = QManipulatorNodeWidget(inp=True, scene=scene)
        self.val2 = QManipulatorNodeWidget(inp=True, scene=scene)
        self.out = QManipulatorNodeWidget(outp=True, scene=scene)
        self.layout.addWidget(self.val1)
        self.layout.addWidget(self.val2)
        self.layout.addWidget(self.out)
        self.out.output = self.output
        self.add_to(scene)

    def output(self):
        if self.val1.input is not None and self.val2.input is not None:
            return self.val1.input + self.val2.input
        return 0.

    def add_to(self, scene):
        item = QNodeProxyWidget(self)
        scene.addItem(item)


class MultNode(AddNode):
    def __init__(self, scene):
        super(MultNode, self).__init__(scene)
        self.titel.setText("Multply")

    def output(self):
        if self.val1.input is not None and self.val2.input is not None:
            return self.val1.input * self.val2.input
        return 0.


def createNodeView(winTitle="NodeView"):
    mdi = getMdiArea()
    if not mdi:
        return None
    scene = QNodeScene()
    view = QNodeView()
    view.setScene(scene)
    view.setWindowTitle(winTitle)
    sub = mdi.addSubWindow(view)

    a = SliderNode(scene)
    c = SliderNode(scene)
    b = AddNode(scene)
    h = MultNode(scene)
    g = GetValueNode(scene)

    sub.show()

createNodeView()
So I think a node editor isn't that much work to implement in FreeCAD (at least this simple dirty experiment wasn't ;) ) ,but I'am not sure if this would really make sense. In blender the nodeeditor is used to simplify the UI. You can group nodes together and reuse them... The basic inputs are values, vectors but also renderings, textures, ... And with sverchok you can even input objects. This is usefull to handle many objects with one slider or so. (Particlesystem...) So a Nodeeditor makes sense for blender-like programms.

In parametric CAD a node-editor could also be useful:
1:
First as a UI to parameters. So you can make a Part or a Assembly out of Nodes (Sketch Node, Boolean Node, Assembly Node...)
For example the sketch node could have properties as inputs, which can be connected to some math Nodes. The sketch node is connected to a Extrude-Node which has a hight input .....
In the end you have a Part. This Part can be grouped, so you only see the inputs that can be changed and the outputs. The ouputs could be a shape. This could be assembled with an other Shape-Node by a Assembly Node....

2:
The Node-Editor would make the Dependency-Graph visibly. This may be an advantage for the programmers, because it would make the linking easier. It also gives the user more possebilities(reuse a sketch, anywhere in the nodesystem, get properties from any node...) So it would extend the freedom how something is modeled.

So I don't know if a nodesystem would really be usefull for freecad, because it would be like a replacement of the traditional parametric-UI and not so much a addition to that.
It would be interesting to get some comments on that and maybe build a concept of a usefull nodeeditor for freecad.

nice regards
and don't forget,
divide the gui from the app
please help with my conda-packaging efforts: https://liberapay.com/looooo/
kianwee
Posts: 10
Joined: Wed May 07, 2014 7:54 am

Re: Visual Programming with Dynamo atop Freecad API?

Postby kianwee » Sun Aug 31, 2014 4:58 am

Just an add on other than dynamo there are other open-source visual programming tools that are platform independent, here are a few:

coral - https://code.google.com/p/coral-repo/

vistrails - http://www.vistrails.org/index.php/Main_Page
looo
Posts: 2917
Joined: Mon Nov 11, 2013 5:29 pm

Re: Visual Programming with Dynamo atop Freecad API?

Postby looo » Sun Aug 31, 2014 9:38 am

Just an add on other than dynamo there are other open-source visual programming tools that are platform independent, here are a few:

coral - https://code.google.com/p/coral-repo/

vistrails - http://www.vistrails.org/index.php/Main_Page
I don't think an extern libary for nodes is needed. The work for the Gui and the connections is less thn the creation of the nodes. So I think it is better to use Qt/PySide for the Gui, and make the logic with python. Here is a picture of some freecad interaction :D :
Image
Last edited by looo on Wed Sep 03, 2014 3:55 pm, edited 1 time in total.
please help with my conda-packaging efforts: https://liberapay.com/looooo/
mrlukeparry
Posts: 655
Joined: Fri Jul 22, 2011 8:37 pm
Contact:

Re: Visual Programming with Dynamo atop Freecad API?

Postby mrlukeparry » Sun Aug 31, 2014 9:50 am

I agree with looo but nonetheless coral looks interesting and might be a starting point to play with something.

The experiment in python is pretty good already just from the screenshot and no framework would be needed if we are manipulating the document object tree and properties. I still hesitate whether this would be useful for traditional workflow.

It's nice to see all the properties are available in a model tree, but without having robust references yet, it would create problems anyway.
User avatar
saso
Posts: 1333
Joined: Fri May 16, 2014 1:14 pm
Contact:

Re: Visual Programming with Dynamo atop Freecad API?

Postby saso » Sun Aug 31, 2014 11:38 am

Stop teasing, I hope you realize that now there is no way back, this just has to be done :)

Here is one more project I know of https://github.com/pboyer/flood
ickby
Posts: 2922
Joined: Wed Oct 05, 2011 7:36 am

Re: Visual Programming with Dynamo atop Freecad API?

Postby ickby » Sun Aug 31, 2014 12:02 pm

Imho this is the wrong approach as you need to redo all commands etc. in a new ui. I think it would be cleverer to make a new representation of the document, a node graph instead of a tree with the properties exposed (beasicly the freecad document objects form a node graph already as can be seen with the dependency graph). Than you can reuse the whole existing gui to add the already existing features, get the automatic recompute and a perfect integration into the normal freecad workflow, have save/load for free etc. Basically all you need is a new widget like the tree view just with a different representation. This can then be based on a QGraphicsView fro dragging and stuff.

EDIT: You've done some nice work, please don't feel discouraged!
looo
Posts: 2917
Joined: Mon Nov 11, 2013 5:29 pm

Re: Visual Programming with Dynamo atop Freecad API?

Postby looo » Sun Aug 31, 2014 12:45 pm

Imho this is the wrong approach as you need to redo all commands etc. in a new ui. I think it would be cleverer to make a new representation of the document, a node graph instead of a tree with the properties exposed (beasicly the freecad document objects form a node graph already as can be seen with the dependency graph). Than you can reuse the whole existing gui to add the already existing features, get the automatic recompute and a perfect integration into the normal freecad workflow, have save/load for free etc. Basically all you need is a new widget like the tree view just with a different representation. This can then be based on a QGraphicsView fro dragging and stuff.
Thats true. I also think that it is not really compatible with the traditional workflow. And another wokbench would not make sense. It's now allready very difficult to undestand why there is Part, Part-design,...
If some day freecad (or pyocc) has ported to python 3, this could be easily implemented into blender, because blender allready provide a node-editor....
EDIT: You've done some nice work, please don't feel discouraged!
thanks, it's always good to get some critics.
please help with my conda-packaging efforts: https://liberapay.com/looooo/
looo
Posts: 2917
Joined: Mon Nov 11, 2013 5:29 pm

Re: Visual Programming with Dynamo atop Freecad API?

Postby looo » Wed Sep 03, 2014 3:35 pm

If someone wants to extend this..., Ive put the actual state of the node-editor experiment on github:
https://github.com/looooo/NodeEditor

to try out link it to the python modules or put the nodeeditor folder into .FreeCAD and type:
import nodeeditor.freecad as nf
nf.NodeWindow()

it's only an experiment, to test the possebilities of such a system and build my mind of how this could be useful. so it has no functionality.

You can create widgets with InputNodes and OutputNodes which are also widgets.
The way how things work is realy easy:
an input get the output of the connected node
the outputfunction manipulates the input and return an object

an output can be conected more thn one time, an input has only one connected line.
thats all

nice regards
please help with my conda-packaging efforts: https://liberapay.com/looooo/