Extending DAVE#

When adding (proprietary) elements to DAVE you may want to extend the GUI without changing the source-code of DAVE itself.

This section describes how to integrate

  • workspaces

  • dockwidgets

  • nodes

into the GUI.

Note

The DAVE gui is open-source. If the functionality is insufficient for your purpose then you may implement this yourself and be added to the DAVE Gui via a PR on github.

New Node types#

Registering#

Registering the class#

DAVE executes python code when importing files, copying the scene or adding nodes.

New classes need to be made available to the environment when running code in order to be usable.

This is done by adding them to a global setting DAVE_ADDITIONAL_RUNTIME_MODULES:

# Add the balloon class to the modules that are available when executing code

from DAVE.settings import DAVE_ADDITIONAL_RUNTIME_MODULES
DAVE_ADDITIONAL_RUNTIME_MODULES['Balloon'] = Balloon

Registering the properties and documentation#

The properties of the and their documentation need to be added to DAVE as well. This makes them available for timelines, limits derived-properties, reporting and others.

The properties are registered in the following nested dictionary:

from DAVE.settings import DAVE_NODEPROP_INFO,NodePropertyInfo

# type is the node-type, for example Point
# property-name is the name of the property, for example 'global_position'
info = DAVE_NODEPROP_INFO[type][property_name]

# info is a NodePropertyInfo object

The properties of nodes are used in various places in the guis and reporting:

  • The limits screen to define limits for single numerical properties

    • Properties with a single numerical value

  • The derived properties screen - to show the values of derived properties

    • Anything interesting, typically numerical and boolean values and sequences of those

      • type = float, bool

  • Reporting module

    • All properties that have a physical meaning and/or define how the model is structured (parent, meshes, etc)

  • Timeline module

    • All settable single properties, including nodes and booleans

      • type = float/bool/node

      • settable = True

  • Exploration module

    • All settable single numerical properties

Besides that we need to have the following documentation available for each node/property combination:

  • short description (1 line)

  • long description

  • unit, eg [m]

  • remarks, eg (global) or (parent)

    Here the property may be inherited and/or overridden.

  • settable: not read-only

  • single-settable: can be used in time-line - bool, Node,

  • single-numeric: can be used as limit

Class

Property

Short doc

Long doc

Unit

Remarks

type

settable

single-settable

single-numeric

Point

x

x-position

x-position…

[m]

(parent)

float

X

X

X

Point

parent

parent

parent…

(Node)

Frame

X

X

Node

name

name

name…

(unique)

str

X

Point

position

position

position…

[m,m,m]

(parent)

float

X

Point

applied_force

force

force …

[kN,kN,kN]

(parent)

float

X

Point

force

|force|

force …

[kN]

float

X

Frame

fixed_x

bool

X

X

Frame

fixed

bool

X

Node

visible

visible in GUI

bool

X

X

Cable

connections

Point|Circle

X

How, where and when#

When looking up the properties this will be done using a Node object and optionally a property name (these two are therefore the index of the database). Matching class needs to be done on an is-instance basis and considering overriding.

Say:

class A has property p
class B derives from A and overrides property p
class C derives from B

How

requesting the documentation for property p on a node of class C should give the documentation of p of class B. This can be done using the mro. The property of the class with the lowest index in the mro of the node class is the one we want.

The data is stored as a nested dictionary:

data[class][propname(str)] = node_property_info

with node_property_info being a NodePropertyInfo dataclass

There is a convenience function to register individual properties:

from DAVE.settings import register_nodeprop

register_nodeprop(HEBO_CraneVessel,'hoist_length_main_sb',
                 property_type=float,
                 doc_short="SB main hoist length",
                 doc_long="SB main hoist length",
                 units = "m",
                 remarks="",
                 is_settable=True,
                 is_single_numeric=True,
                 is_single_settable=True)

But if the docstrings and type-hints are correct, then the code to do this can be automatically generated via DAVE.helpers.generate_node_documentation.generate_python_code

from DAVE import Point
from DAVE.helpers.generate_node_documentation import generate_python_code

code = generate_python_code(Point) #<-- automatically geneartes the code

filename = r"C:\data\demo.py"
with open(filename,'w') as f:
    f.write('\n'.join(code))

Defining the graphics for a new node#

The new node needs to be painted, labeled and shown in the node tree.

Both the paint and the icon of a node are defined in settings_visual. The icons are defined in a dict

ICONS[type(node)] = QIcon(...)

The paint is defined in the PAINTERS dictionary. This dictionary is organized by class-name (str). A convenience function AddPaintForNodeClassAsCopyOfOtherClass is available to quickly define a copy of an existing paint.

Icons can only be created when an QApplication is present, so one needs to be created before defining the icon.

However, Gui will loop through the ICONS dictionary in the constructor and will replace and string values entries with QIcon(that string). So setting the icon as a string (as below) also works and is preferred:

from DAVE.settings_visuals import AddPaintForNodeClassAsCopyOfOtherClass, ICONS

# Define paint
AddPaintForNodeClassAsCopyOfOtherClass('Balloon','Frame')

# Icons
# if QApplication.instance() is None:
#    app = QApplication()
#    
# ICONS[Balloon] = QIcon(':/icons/Balloon.png')

ICONS[Balloon] = ':/icons/Balloon.png'

Creating a new dock#

Docks are floating or docked sub-windows of the main GUI. Everything except the 3d view-port and the menu is a dock.

Docks are QtWidgets derived from guiDockWidget

Docks are created and activated by main. Upon creation they receive some references to things in the gui:

d.guiScene = self.scene
d.guiEmitEvent = self.guiEmitEvent
d.guiRunCodeCallback = self.run_code
d.guiSelectNode = self.guiSelectNode
d.guiSelection = self.selected_nodes
d.guiPressSolveButton = self.solve_statics
d.gui = self

Communication between the docks is done via the guiEmitEvent which can send any of the ENUMs defined in guiEventType. Handling of these events is to be implemented in guiProcessEvent.

Registering#

Any dock needs to be added to the DAVE_GUI_DOCKS dictionary. The keys in this dictionary are strings. These strings are also used as the dock-window title.

from DAVE.gui.dockwidget import DAVE_GUI_DOCKS
DAVE_GUI_DOCKS['Balloon'] = BalloonDock

Activating a dock#

See workspaces

Integrating into the main gui#

Code can be executed at various moment by defining plugin-functions and adding a reference to that function to a globally available list.

Init#

plugin_init is executed at the end of the .init function. It takes a single parameter being the instance of the Gui that is being intialized ( the self of the init function)

def my_function(gui):
	print('executed on startup')

from DAVE.gui.main import DAVE_GUI_PLUGINS_INIT
DAVE_GUI_PLUGINS_INIT.append(my_function)

Context-menu#

The context-menu is the right-click menu. This is used to add nodes. A plugin here has the following signature:

openContextMenyAt(menu, node_name, gui)

the plugin gets called with the following arguments:

  1. QMenu object

  2. The name of the first selected node (if any)

  3. A reference to the main gui

the reference to the main gui can be used to run code as follows:

def plugin_context(menu, node_name,gui):
    code = 'Balloon(s, s.available_name_like("new_balloon"))'
    def action():
        gui.run_code(code, guiEventType.MODEL_STRUCTURE_CHANGED)

    BalloonIcon = QIcon(str(Path(__file__).parent / 'balloon.png'))

    action = menu.addAction("Add balloon", action)
    action.setIcon(BalloonIcon)

The plugin needs to be registered in:

from DAVE.gui.main import DAVE_GUI_PLUGINS_CONTEXT
DAVE_GUI_PLUGINS_CONTEXT.append(plugin_context)

Editing nodes#

Selecting a node while in “construction” makes the “Properties” dock-widget visible.

Extending the node-editor#

Sections in the node-propeties widget derive from NodeEditor

  1. Create a new MyNewNodeEditor class which derives from NodeEditor

  2. Register the node-editor class against the node-instances for which the editor should open.

from DAVE.gui.widget_nodeprops import DAVE_GUI_NODE_EDITORS
DAVE_GUI_NODE_EDITORS[Balloon] = BalloonNodeEditor

# Note: Balloon is the Node-Class, BalloonNodeEditor is the editor class

images/image-20220203143548291

Alternatively#

Create a new dock that reacts to the “SELECTION_CHANGED” event.