2
$\begingroup$

My add-on acts upon a hierarchy of data (a tree). I am not sure how best to represent this hierarchy as properties to expose the data to the user.

For simplicity, let us define an example: say that each node of the tree has a "value" which for example purposes is a float. Each node also has zero, one, or many child nodes. As I understand it, properties must all be registered up front in register(), which is the problem -- I guess I need to register all the properties for all the children that could ever exist?

I have had good results with conditionally presenting UI elements in draw(), and so I thought that one approach could be to add an "isEnabled" flag to the node class and define the tree as having a certain maximum depth with each node having a maximum number of children. If in our example the maximum tree that we require has a max depth of 2 and max children of 5, we end up with 31 nodes: the root, the root's 5 children, and then each child has 5 children.

Then, for every node in the tree, for node.value and node.isEnabled register a property in register(). In draw, we can use the isEnabled flag to show or hide the children of a node, which kind of expands or collapses the tree.

Is there a cleaner way to map this kind of data to Blender properties?

Edit: I learned about PropertyGroup, but as I expected it doesn't like circular references.

class MyNode(bpy.types.PropertyGroup):
    value = bpy.props.IntProperty(name="value", default=1, min=1, max=1000, description="")
    child1 = bpy.props.PointerProperty(type=MyNode)

results in:

NameError: name 'MyNode' is not defined

But I think you could define classes for each level:

      A              MyRoot
   ___|___
  /       \
  B       C          MyTrunk
  |       |
 / \     / \
D   E   F   G        MyLeaf

I think that MyRoot can have a bpy.props.CollectionProperty(type=MyTrunk) and MyTrunk each have a collection of MyLeaf and then in register(), on single PointerProperty could be registered of type MyRoot.

This is pretty clean, except that you need to define a class for each level of the hierarchy. Therefore the hierarchy must necessarily be short and the maximum depth limited in advance (by the number of classes you're willing to define).

$\endgroup$
3
$\begingroup$

Leverage the collection

Collections in 2.8 have a hierarchy, well at least a children collection.

enter image description here Result of running test script

  • Can add custom properties to the collection.
  • Collections do not have a parent property, since they do not follow the single parent rule. Have added one.
  • Tagged the collection as "node" by adding an instance only custom property node["node"]

  • Set the root node parent to scene collection for visibility sake, similarly if the parent attribute is set to None

Test script.

import bpy
from bpy.props import FloatProperty, PointerProperty

# clean to test
for c in bpy.data.collections:
    if "node" in c.keys():
        bpy.data.collections.remove(c)


def link_children(self, context):
    if self.parent == self:
        self.parent = None
    if self.parent:
        print(self.name in self.parent.children)
        if self.name not in self.parent.children:
            self.parent.children.link(self)
    else:
        self.parent = context.scene.collection

    mynodes = [n for n in bpy.data.collections if "node" in n.keys()]
    for n in mynodes:

        for c in n.children:
            if c.parent is not n:
                n.children.unlink(c)
        # TODO remove any "dead roots" in scene collection 
    return None

float_prop = FloatProperty()
bpy.types.Collection.float_prop = float_prop
bpy.types.Collection.parent = PointerProperty(type=bpy.types.Collection, update=link_children)

def mynode(name, scene=bpy.context.scene):
    node = bpy.data.collections.new(name)
    node["node"] = True
    return node


scene = bpy.context.scene
my_root = mynode("My Root")
my_root.parent = scene.collection
my_root.float_prop = 1

for i in range(2):
    my_trunk = mynode(f"My Trunk {i}")
    my_trunk.float_prop = 2
    my_trunk.parent = my_root
    for j in range(2):
        my_leaf = mynode(f"My Leaf {i}.{j}")
        my_leaf.float_prop = 3
        my_leaf.parent = my_trunk    


def draw_node(self, context):
    col = context.collection
    if col and "node" in col.keys():
        layout.prop(col, "float_prop")

Drawing in the UI.

Unfortunately the outliner doesn't currently have a way to display properties. Can draw in UI with something like this, here I've used the context.collection and if it is a "node" display the float property

def draw_node(self, context):
    col = context.collection
    if col and "node" in col.keys():
        layout.prop(col, "float_prop")

Extending:

Using old fashioned properties, or getters setters with bpy.props can extend this to have what appears to be its own API, for example

enter image description here Creating an API with a similar feel

from collections.abc import Mapping
import bpy

class MyTreeNodes(Mapping):
    @property
    def _storage(self):
        return {n.name : MyTreeItem(n) for n in bpy.data.collections if "node" in n.keys()}

    def new(self, name):
        node = bpy.data.collections.new(name)
        node["node"] = True
        node.parent = self.id_data.collection
        return MyTreeItem(node)


    def __init__(self,  scene, *args, **kw):
        self.id_data = scene
        self._liststorage = list()
        #self._storage = dict(*args, **kw)

    def __getitem__(self, key):
        if isinstance(key, int):
            return list(self._storage.values())[key]
        return self._storage[key]

    def __iter__(self):
        return iter(self._storage)

    def __len__(self):
        return len(self._storage)

class MyTreeItem(bpy.types.Collection):
    def insert(self, name):
        pass

    def __init__(self, coll):
        self.is_node = "node" in self.keys()


def getnodes(self):
    return MyTreeNodes(self.id_data)

class NodeTree(bpy.types.PropertyGroup):
    nodes = property(getnodes)

bpy.utils.register_class(NodeTree)    
bpy.types.Scene.mytree = bpy.props.PointerProperty(type=NodeTree)

Code below pasted into python console, shown in image above

coll = C.collection
coll
mytree = C.scene.mytree
mytree
nodes = mytree.nodes
foo = nodes.new("foobar")
foo.parent = coll
nodes[0]
nodes['
$\endgroup$
  • $\begingroup$ Your stuff is always so useful .. could you, in principle, create your own subclass NodeProperty of, say,PointerProperty? Or doesn't the API let you in that far? $\endgroup$ – Robin Betts Jun 4 at 10:33
  • $\begingroup$ Not sure I get what you are after? The bpy.props properties set up properties on the classes just how blender likes them to display in UI etc. And in particular store data in blend file. Added example to answer above where context.scene.mytree.nodes is a dictionary mapping to make it function similarlyish to say node_tree.nodes. $\endgroup$ – batFINGER Jun 4 at 11:03
  • $\begingroup$ Wow. Thanks. I'll have a play, to try and get my head round it. $\endgroup$ – Robin Betts Jun 4 at 11:12
  • 1
    $\begingroup$ Whoops got to remember not to swear via print statements in side cases lol. $\endgroup$ – batFINGER Jun 4 at 11:16
  • $\begingroup$ That's really interesting. I'm not using 2.80 yet, but I intend to switch when the release is final. $\endgroup$ – slerfsterdn Jun 4 at 15:48
1
$\begingroup$

I have found that one way around the circular class registration issue is to monkey patch the node children properties after your PropertyGroup class definition, like this:

import bpy

# Create your tree node class
class MyNode(bpy.types.PropertyGroup):
    # node properties here
    pass

# Monkey patch the children property to avoid circular registration
MyNode.children = bpy.props.CollectionProperty(type=MyNode)

# Register and use as usual
bpy.utils.register_class(MyNode)
bpy.types.Scene.tree_root = bpy.props.PointerProperty(type=MyNode)

Use in console/addon:

context.scene.tree_root.name = "A big, muscular father"
new_child = context.scene.tree_root.children.add()
new_child.name = 'A big, muscular baby'
$\endgroup$
  • $\begingroup$ That's a neat trick -- does Blender persist the monkey-patched field(s) correctly? $\endgroup$ – slerfsterdn Jul 12 at 22:01
  • $\begingroup$ @slerfsterdn It appears so. When I run the above code in console -> save as .blend -> close Blender -> reopen .blend -> rerun registration block -> context.scene.tree_root.children[0].name returns 'A big, muscular baby' as expected. $\endgroup$ – Justas Jul 12 at 23:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.