Dynamic Menu Creation in Blender

During my current batch at the Recurse Center, I've been exploring Blender as the UI for a constraint solver I rewrote in Cython. I chose Blender because it's versatile, open source, and free. Unfortunately, one of the most challenging things about developing in Blender is its versatility. I've barely scraped the surface of what it can do, and have mostly been approaching the implementation as: I want to do X, how can I do this with Blender (vs. Rhino, with which I'm much more familiar)?

While I could see how many different options there were for the UI components, many of the add-ons I found for getting started were way too simple. Doing multi-file add-ons is beyond the scope of most tutorials, and Blender has poor documentation for even setting up your development environment. Blender also has its own flavor of Python in terms of object storage and access, which can also be quite confusing. For my application, rather than rewriting a bunch of boilerplate per type of constraint and menu/panel element, I wanted to be able to automatically generate menus from a json file and store all of the custom types of values that would exist in the system (PropertyGroups in Blender speak).

To be honest, half the battle was figuring out what exact term I needed to search; the other half of the battle was finding it for a new enough version of Blender. I ended up stitching together two different blog posts that had tackled this issue.

Kenton's blog post almost works as is (I particularly like that 5 years ago he was in the same position as I am now - "wading through a lot of half-expired forum posts", but such is life in open source). The piece that you need is to change is where you build the property group class, as now you need to provide the attributes dictionary as the value to the __annotations__ key.

propertyGroupClass = type(groupName, (PropertyGroup,), {'__annotations__': attributes})

I also changed my __init__ file to load the submenus directly from a json file (Kenton has a comment in his blog post to implement this). Here are the building blocks for my menus based on a multi-file add-on, where the UI and operators are separated into their own files.

blender_pymaxion/assets/property_groups.json

{
  "Anchor": [
    {
      "name": "base",
      "type": "float",
      "min": 1,
      "max": 10,
      "default": 1,
      "precision": 2
    },
    {
      "name": "power",
      "type": "int",
      "min": -30,
      "max": 30,
      "default": 20
    }
  ]
}

blender_pymaxion/ui.py

import bpy
from bpy.types import Panel

class PYMAXION_PT_Anchor(Panel):
    bl_label = "Add Anchors"
    bl_idname = "PYMAXION_PT_Anchor"
    bl_options = {"INSTANCED"}
    bl_space_type = "VIEW_3D"
    bl_region_type = "WINDOW"

    def draw(self, context):
        layout = self.layout
        scene = context.scene
        propertyGroup = getattr(scene, "Anchor")

        layout.label(text="Add Anchor Constraint")
        row = layout.row(align=True)
        row.prop(propertyGroup, "base", text="Strength Value")
        row.prop(propertyGroup, "power", text="10^")
        row = layout.row(align=True)
        row.operator("pymaxion_blender.anchor_constraint", text="Add").action = "ADD"

blender_pymaxion/__init__.py

import os
import sys
import json

bpy = sys.modules.get("bpy")

if bpy is not None:
    import bpy
    from bpy.utils import register_class
    from bpy.utils import unregister_class
    from bpy.types import PropertyGroup
    from bpy.props import FloatProperty
    from bpy.props import IntProperty
    from bpy.props import StringProperty
    from bpy.props import PointerProperty

    # Check if this add-on is being reloaded
    # *This is only meant for development purposes*
    if "ui" not in locals():
        from . import ui

    else:
        import importlib
        ui = importlib.reload(ui)
        operator = importlib.reload(operator)

    classes = (
        ui.PYMAXION_PT_Anchor,
    )

    bpy.propertyGroups = {}

    def register():
        for cls in classes:
            register_class(cls)

        # Load custom property groups from json
        dir_path = os.path.dirname(os.path.realpath(__file__))
        with open(dir_path + '/assets/property_groups.json', 'r') as f:
            properties = json.load(f)

        for groupName, attributeDefinitions in properties.items():
            attributes = {}
            for attributeDefinition in attributeDefinitions:
                attType = attributeDefinition['type']
                attName = attributeDefinition['name']
                # Note: The type of attribute must be removed for the PropertyGroup class
                # as it is not a valid key for its attribute dictionary
                # Additional attribute types can be added here to be properly handled
                if attType == 'float':
                    attributeDefinition.pop('type')
                    attributes[attName] = (FloatProperty, attributeDefinition)
                elif attType == 'int':
                    attributeDefinition.pop('type')
                    attributes[attName] = (IntProperty, attributeDefinition)
                else:
                    raise TypeError('Unsupported type (%s) for %s on %s!' % (attType, attName, groupName))
            # the type declaration below is what has changed from Blender 2.6
            propertyGroupClass = type(groupName, (PropertyGroup,), {'__annotations__': attributes})
            bpy.utils.register_class(propertyGroupClass)
            setattr(bpy.types.Scene, groupName, PointerProperty(type=propertyGroupClass))
            bpy.propertyGroups[groupName] = propertyGroupClass

    def unregister():
        for cls in reversed(classes):
            unregister_class(cls)

        try:
            for key, value in bpy.propertyGroups.items():
                delattr(bpy.types.Object, key)
                unregister_class(value)
        except UnboundLocalError:
            pass

While I'm happy with the reduction in code to creating custom properties, I still think that I could create a panel factory to further reduce code declarations within the UI, since the panels for each constraint type will be quite similar.

Anchor Panel Menu