© Chris Conlan 2017

Chris Conlan, The Blender Python API, 10.1007/978-1-4842-2802-9_5

5. Introduction to Add-On Development

Chris Conlan

(1)Bethesda, Maryland, USA

This chapter builds basic add-ons using using Blender’s Python API. One of the biggest hurdles of add-on development is transitioning from a development environment to a neatly packaged and OS-independent add-on, so we spend considerable time in this chapter discussing various development practices. By the end of the chapter, readers should be able to register simple add-ons in both development and deployment environments. Following chapters build on this knowledge to incorporate more advanced features into add-ons.

A Simple Add-On Template

For this section, enter the scripting view in Blender and go to Text Editor ➤ New to create a new script. Give it a name, for example, simpleaddon.py. See Listing 5-1 for a simple template from where we can start building our add-on. Running this script will create a new tab in the Tools panel called “Simple Addon” that has a simple text input field and a button. The button will print a message to the console verifying that the plugin works, then parrot back the string in the text input field. See Figure 5-1 for the appearance and location of the add-on’s GUI.

A438961_1_En_5_Fig1_HTML.jpg
Figure 5-1. Simple add-on template
Listing 5-1. Simple Add-On Template
bl_info = {
    "name": "Simple Add-on Template",
    "author": "Chris Conlan",
    "location": "View3D > Tools > Simple Addon",
    "version": (1, 0, 0),
    "blender": (2, 7, 8),
    "description": "Starting point for new add-ons.",
    "wiki_url": "http://example.com",
    "category": "Development"
}


# Custom modules are imported here
# See end of chapter example for suggested protocol


import bpy

# Panels, buttons, operators, menus, and
# functions are all declared in this area


# A simple Operator class

class SimpleOperator(bpy.types.Operator):
    bl_idname = "object.simple_operator"
    bl_label = "Print an Encouraging Message"


    def execute(self, context):
        print(" ####################################################")
        print("# Add-on and Simple Operator executed successfully!")
        print("# " + context.scene.encouraging_message)
        print("####################################################")
        return {'FINISHED'}


    @classmethod
    def register(cls):
        print("Registered class: %s " % cls.bl_label)


        # Register properties related to the class here
        bpy.types.Scene.encouraging_message = bpy.props.StringProperty(
            name="",
            description="Message to print to user",
            default="Have a nice day!")


    @classmethod
    def unregister(cls):
        print("Unregistered class: %s " % cls.bl_label)


        # Delete parameters related to the class here
        del bpy.types.Scene.encouraging_message


# A simple button and input field in the Tools panel
class SimplePanel(bpy.types.Panel):
    bl_space_type = "VIEW_3D"
    bl_region_type = "TOOLS"
    bl_category = "Simple Addon"
    bl_label = "Call Simple Operator"
    bl_context = "objectmode"


    def draw(self, context):
        self.layout.operator("object.simple_operator",
                             text="Print Encouraging Message")
        self.layout.prop(context.scene, 'encouraging_message')


    @classmethod
    def register(cls):
        print("Registered class: %s " % cls.bl_label)
        # Register properties related to the class here.


    @classmethod
    def unregister(cls):
        print("Unregistered class: %s " % cls.bl_label)
        # Delete parameters related to the class here


def register():

    # Implicitly register objects inheriting bpy.types in current file and scope
    #bpy.utils.register_module(__name__)


    # Or explicitly register objects
    bpy.utils.register_class(SimpleOperator)
    bpy.utils.register_class(SimplePanel)


    print("%s registration complete " % bl_info.get('name'))

def unregister():

    # Always unregister in reverse order to prevent error due to
    # interdependencies


    # Explicitly unregister objects
    # bpy.utils.unregister_class(SimpleOperator)
    # bpy.utils.unregister_class(SimplePanel)


    # Or unregister objects inheriting bpy.types in current file and scope
    bpy.utils.unregister_module(__name__)
    print("%s unregister complete " % bl_info.get('name'))


# Only called during development with 'Text Editor -> Run Script'
# When distributed as plugin, Blender will directly
# and call register() and unregister()
if __name__ == "__main__":


    try:
        unregister()
    except Exception as e:
        # Catch failure to unregister explicitly
        print(e)
        pass


    register()

When we run the script, we should get console output about the registration and unregistration of the classes we declared in Listing 5-1. By changing the messages and choosing Print Encouraging Message, we should get something like the following in the console:

Unregistered class: Print an Encouraging Message Unregistered class: Call Simple Operator
Simple Add-on Template unregister complete


Registered class: Print an Encouraging Message Registered class: Call Simple Operator
Simple Add-on Template registration complete


####################################################
# Add-on and Simple Operator executed successfully!
# Have a nice day!


####################################################

####################################################
# Add-on and Simple Operator executed successfully!
# I changed the message!
####################################################

Though there are many specifics to explain, Blender add-ons are fairly elegant and readable. While every line of code has a purpose, the scripts benefit from consistency via repetition. The template presented in Figure 5-1 is fairly minimal, but we also included a handful of optional quality controls. We discuss each component before proceeding to more advanced add-ons .

Components of Blender Add-Ons

Blender add-ons rely on many different and specifically named variables and class functions to operate properly. We detail them by category here.

The bl_info Dictionary

The first thing to appear in a Blender add-on should be the bl_info dictionary . This dictionary is parsed from the first 1024 bytes of the source file so it is imperative that bl_info appear at the top of the file. We will use the word dictionary to refer to Python objects of class dict in writing.

Blender’s internal engine uses data in this dictionary to populate various metadata related to the add-on itself. If we navigate to Header Menu ➤ File ➤ User Preferences ➤ Add-ons, we can see various official and community add-ons already in Blender. Clicking the caret on any of the add-ons shows how bl_info information is used to populate this GUI, as shown in Figure 5-2.

A438961_1_En_5_Fig2_HTML.jpg
Figure 5-2. How Blender uses bl_info

It is important to note that the bl_info dictionary does not have any functional bearing on the add-on, rather it determines how the eventual user can find and activate it in this window. See the detailed description here:

  • name The name of the plugin as it appears in the add-ons tab of the user preferences (e.g., Math Vis (Console), Motion Capture Tools). It is written as a single string.

  • author The Name of the author or authors as it appears in the user preferences (e.g., Campbell Barton, Fabian Fricke). It can be a string with commas or a tuple of strings.

  • location The primary location of the add-on’s GUI. Common syntax is Window ➤ Panel ➤ Tab ➤ Section for add-ons in the Tools, Properties, and Toolshelf panels. When in doubt, follow conventions established by other add-ons.

  • version The version number of the add-on as a tuple.

  • blender According to the Blender Wiki, this is the minimum Blender version number required to run the add-on. Community add-ons often falsely list (2, 7, 8) as the version when lower versions can support an add-on. In many cases the number refers the minimum version the developer has chosen to support.

  • description A brief description that appears in the user preferences window specified as a single string.

  • wiki_url—An URL pointing to the handbook or guide for the add-on specified as a single string.

  • category A string specifying one the categories listed in Table 5-1.

    Table 5-1. The bl-info Category Options

    3D View

    Compositing

    Lighting

    Object

    Rigging

    Text Editor

    Add Mesh

    Development

    Material

    Paint

    Scene

    UV

    Add Curve

    Game Engine

    Mesh

    Physics

    Sequencer

    User Interface

    Animation

    Import-Export

    Node

    Render

    System

     

There are a few remaining bl_info options that are less often seen.

  • support OFFICIAL, COMMUNITY, or TESTING. Where official refers to officially supported Blender add-ons, community refers community-supported add-ons, and testing refers to unfinished or new add-ons that should be intentionally excluded from Blender releases.

  • tracker_url URL pointing to a bug tracker (e.g., GitHub issues or similar).

  • warning String specifying some warning that will appear in the user preferences window.

Operators and Class Inheritance (bpy.types.Operator )

In the simplest sense, add-ons allow us to call Blender Python functions by clicking a button in the standard Blender GUI. Functions called by the Blender GUI must first be registered as operators of class bpy.types.Operator. Take for example SimpleOperator. When we register this class, the call to SimpleOperator.execute() is mapped to a function object in bpy.ops. The function is bpy.ops that it is mapped to is determined by the bl_idname value at the head of the class. Thus, after you run the script in Listing 5-1, you can print an encouraging message by calling bpy.ops.object.simple_operator() from the Interactive Console, from the add-on itself, or from unrelated Python scripts.

The following are the steps to declare an operator in Blender. Refer to the SimpleOperator class definition in Listing 5-1 throughout.

  1. Declare a class that inherits bpy.types.Operator. This will appear in our code as:

    class MyNewOperator                                              (bpy.types.Operator):
  2. Declare bl_idname as a string with class and function name of your choice, separated by a period (e.g., object.simple_operator or simple.message). The class and function names can only contain lowercase characters and underscores. The execute function will later be accessible at bpy.ops.my_bl_idname.

  3. (Optional) Declare a bl_label as any string describing the function of the class. This will appear in function documentation and metadata automatically generated by Blender.

  4. Declare an execute function. This function will act as a normal class function and will always accept a reference to bpy.context as a parameter. By design of the bpy.types.Operator class, the execute function will always be defined as:

    def execute(self, context):

    It is best practice to return {"FINISHED"} for a successful call to execute() within an operator class.

  5. (Optional) Declare class methods for registering and unregistering the class. The register and unregister functions will always require the @classmethod decorator and take cls as an argument. These functions are run whenever Blender attempts to register or unregister the operator class. It is helpful during development to include a print statement about class registration and deregistration as we have done in Listing 5-1 to check that Blender is not mistakenly reregistering existing classes. It is also important to note that we can declare and delete scene properties in these functions. We discuss this in later sections .

There are a handful of restrictions and guidelines to follow to ensure Blender can use our Python code. Ultimately, these guidelines change the way we code and the way we think about architecting Python codebases. This is the point in our understanding of the Blender Python API where it starts to feel like a true application programming interface (API) rather than just a collection of useful functions .

Panels and Class Inheritance (bpy.types.Panel)

The bpy.types.Panel class is next most common class inherited in add-ons. Panels already make up the majority of Blender’s Tools, Toolshelf, and Properties windows. Each collapsible section of one of these windows is a distinct panel. For example, if we navigate to 3D Viewport ➤ Tools ➤ Tools we see three panels by default: Transform, Edit, and History. Within a Blender Python add-on, these would be represented by three distinct bpy.types.Panel classes.

Here are the requirements to register a panel. Reference the SimplePanel class in Listing 5-1 throughout.

  1. Declare a class that inherits bpy.types.Panel. This will appear as class MyNewPanel(bpy.types.Panel):.

  2. Declare bl_space_type, bl_region_type, bl_category, and bl_label. Readers may have noticed the ordering of these is intentional (though not necessary). These four variables, in the order written and in Listing 5-1, specify the path that the user takes to reach the panel. In Listing 5-1, this reads VIEW_3D ➤ TOOLS ➤ Simple Addon ➤ Call Simple Operator, which looks very familiar to the way we have located GUI elements thus far in the text. Correct case and spelling matter in these variables. While the category and label can be arbitrary values, the space and region must reference real areas of the Blender GUI. See Tables 5-2 and 5-3 for the list of possible arguments to bl_space_type and bl_region_type.

    Table 5-2. bl-space-type Options

    EMPTY

    NLA_EDITOR

    NODE_EDITOR

    INFO

    VIEW_3D

    IMAGE_EDITOR

    LOGIC_EDITOR

    FILE_BROWSER

    TIMELINE

    SEQUENCE_EDITOR

    PROPERTIES

    CONSOLE

    GRAPH_EDITOR

    CLIP_EDITOR

    OUTLINER

     

    DOPESHEET_EDITOR

    TEXT_EDITOR

    USER_PREFERENCES

     
    Table 5-3. bl-region-type Options

    WINDOW

    HEADER

    CHANNELS

    TEMPORARY UI

    TOOLS

    TOOL_PROPS

    PREVIEW

    Most combinations of bl_space_type and bl_region_type do not work together, but logical combinations will generally work. There is presently no complete documentation on which space types and region types cooperate. Also, not all space types and region types require a declaration of bl_category or bl_label. Again, using them where logical typically gives good results.

  3. (Optional) Declare bl_context. As in the previous example, we can set bl_context equal to objectmode to make the panel only appear in Object Mode. As of the time of writing, we do not have a concrete list of valid options for this variable. The API documentation currently has a TODO tag requesting more explanation. We introduce in a later chapter the poll() method, which is a much more flexible way of implementing this type of behavior .

  4. Declare the draw method. This function takes the context as a parameter and will always be declared as def draw(self, context):. In this function definition, it is important to note that context refers to the bpy.context object but should not be passed as bpy.context. The important variables in the body of this function are bpy.context.scene and self.layout. The layout.prop() function can reference scene properties, object properties, and a few other Blender internal properties. It will automatically create the appropriate input field based on the scene property itself. The encouraging_message scene property in Listing 5-1 was declared as a string property, so supplying it as an argument to layout.prop() produced a text entry field. The layout.operator() function takes the bl_idname of an operator and creates a button with label specified by the text = argument. We will not go into detail about the layout object here, because it can get very complex for advanced GUIs. We discuss the layout object in detail later in this chapter.

  5. (Optional) Declare register() and unregister() functions with decorator @classmethod, as in our discussion of bpy.types.Operator classes.

Register() and Unregister()

Near the end of Listing 5-1 are two functions, register() and unregister(), that are required in add-ons. These two functions are responsible for calling bpy.utils.register_class(), bpy.utils.unregister_class(), bpy.utils.register_module(), and bpy.utils.unregister_module(). Any class that inherits a bpy.type class needs to be registered for it to be used by Blender in the add-on. Blender uses the unregister() function when an add-on is switched off by the user in the user preferences.

We have two choices for registering and unregistering classes. Some work better than others for development and others work better for deployment.

  • Explicitly register and unregister each class. In this case, we want to register classes in a logical sequence. Classes that depend on others should be registered after their dependents. We do this in the register() function using bpy.utils.register_class(), passing the class name as an argument. The classes should be unregistered in reverse order using bpy.utils.unregister_class() in the unregister() function.

  • Implicitly register and unregister classes according to its membership in a module. We do so with the bpy.utils.register_module() and bpy.utils.unregister_module() functions. We often see bpy.utils.register_module(__name__) called in the register() function of published add-ons, but it can be messy during development, as we explain shortly .

Looking back at Listing 5-1, we see that we have explicitly registered but implicitly unregistered our classes. This setup is, in the author’s opinion, ideal for live editing of single-file add-ons. The bpy.utils.unregister_module(__name__) works as intended to clear the add-on environment of classes registered in previous runs of the script. During editing done using Blender’s Text Editor, bpy.utils.register_module(__name__) often registers dead or unused copies of classes from previous runs of the script.

Therefore, the clean slate approach to live editing add-ons seems to be explicitly registering and implicitly deregistering. Implicit deregistration will pick up stray class instances from previous runs, then explicit registration instantiates only the newly created classes from the current run. This goes against the advice of most documentation, which typically suggests registering and deregistering using one of the styles in Listing 5-2. Our methods in Listing 5-1 are safe, verbose, and can be easily modified to conform to the commonly accepted practices in Listing 5-2.

Listing 5-2. Registration Protocol
# Option 1:                  
# Using implicit registration


def register():
    bpy.utils.register_module(__name__)


def unregister():
    bpy.utils.unregister_module(__name__)


if __name__ == "__main__":
    register()


# Option 2:
# Using explicit registration


def register():
    bpy.utils.register_class(SimpleOperator)
    bpy.utils.register_class(SimplePanel)


def unregister():
    bpy.utils.unregister_class(SimpleOperator)
    bpy.utils.unregister_class(SimplePanel)


if __name__ == "__main__":
    register()
# Option 3 (Recommended)
# Explicit registration and implicit unregistration
# With safe + verbose single-script run


def register():
    bpy.utils.register_class(SimpleOperator)
    bpy.utils.register_class(SimplePanel)


def unregister():
    bpy.utils.unregister_module(__name__)


if __name__ == "__main__":
    try:
        unregister()
    except Exception as e:
        print(e)
        pass


    register()

Scene Properties and bpy.props

Properties that are added to the Scene and Object types will be saved to the .blend file. In order for users to modify variables via the Blender GUI, they must be registered as bpy.props.* objects. The bpy.props class has options for most data types, including floats, integers, strings, and Booleans. They may be registered to bpy.types.* classes, including Scene and Object. In this section, we discuss how to register simple scene properties to bpy.types.Scene.* variables. These are arbitrarily named variables that are accessible via bpy.context.scene.*. While the name is arbitrary, it is restricted to lowercase characters and underscores.

There are two places we can register scene variables:

  • In the register() function at the bottom of the script.

  • In the register() classmethod of any class that inherits a bpy.types.* class (panels, operators, menus, etc.).

Most commonly, scene variables are tied directly to a class. For sake of clarity and organization, we want to declare those variables within the register() classmethod of that class. Other variables that do not fit neatly into a class definition can be declared in the register() function at the bottom of the script. In this text, we encourage that scene properties are declared in the register() classmethod if closely associated with a specific class, but this is not commonly seen in existing community add-ons .

Scene variables will be instances of bpy.types.* variables. These include the Blender types StringProperty, FloatProperty, IntProperty, and BoolProperty. Any time a panel includes a variable in a GUI via a call to self.layout.prop, the variable will be logically formatted according to its type. Integers and floats appear in slider bars, strings appear as text input fields, Booleans appear as checkboxes, and so on.

In Listing 5-3, we redeclare SimpleOperator and SimplePanel from Listing 5-1 with additional scene variables. Readers will rewrite these classes using Listing 5-1 as a template. See Figure 5-3 for the resulting GUI.

A438961_1_En_5_Fig3_HTML.jpg
Figure 5-3. Exploring scene properties
Listing 5-3. Exploring Scene Properties
# Simple Operator with Extra Properties                  
class SimpleOperator(bpy.types.Operator):
    bl_idname = "object.simple_operator"
    bl_label = "Print an Encouraging Message"


    def execute(self, context):
        print(" ####################################################")
        print("# Add-on and Simple Operator executed successfully!")
        print("# Encouraging Message:", context.scene.encouraging_message)
        print("# My Int:", context.scene.my_int_prop)
        print("# My Float:", context.scene.my_float_prop)
        print("# My Bool:", context.scene.my_bool_prop)
        print("# My Int Vector:", *context.scene.my_int_vector_prop)
        print("# My Float Vector:", *context.scene.my_float_vector_prop)
        print("# My Bool Vector:", *context.scene.my_bool_vector_prop)
        print("####################################################")
        return {'FINISHED'}


    @classmethod
    def register(cls):
        print("Registered class: %s " % cls.bl_label)


        bpy.types.Scene.encouraging_message = bpy.props.StringProperty(
            name="",
            description="Message to print to user",
            default="Have a nice day!")


        bpy.types.Scene.my_int_prop = bpy.props.IntProperty(
            name="My Int",
            description="Sample integer property to print to user",
            default=123,
            min=100,
            max=200)


        bpy.types.Scene.my_float_prop = bpy.props.FloatProperty(
            name="My Float",
            description="Sample float property to print to user",
            default=3.1415,
            min=0.0,
            max=10.0,
            precision=4)


        bpy.types.Scene.my_bool_prop = bpy.props.BoolProperty(
            name="My Bool",
            description="Sample boolean property to print to user",
            default=True)


        bpy.types.Scene.my_int_vector_prop = bpy.props.IntVectorProperty(
            name="My Int Vector",
            description="Sample integer vector property to print to user",
            default=(1, 2, 3, 4),
            subtype='NONE',
            size=4)


        bpy.types.Scene.my_float_vector_prop = bpy.props.FloatVectorProperty(
            name="My Float Vector",
            description="Sample float vector property to print to user",
            default=(1.23, 2.34, 3.45),
            subtype='XYZ',
            size=3,
            precision=2)


        bpy.types.Scene.my_bool_vector_prop = bpy.props.BoolVectorProperty(
            name="My Bool Vector",
            description="Sample bool vector property to print to user",
            default=(True, False, True),
            subtype='XYZ',
            size=3)


    @classmethod
    def unregister(cls):
        print("Unregistered class: %s " % cls.bl_label)
        del bpy.types.Scene.encouraging_message


# Simple button in Tools panel
class SimplePanel(bpy.types.Panel):
    bl_space_type = "VIEW_3D"
    bl_region_type = "TOOLS"
    bl_category = "Simple Addon"
    bl_label = "Call Simple Operator"
    bl_context = "objectmode"


    def draw(self, context):
        self.layout.operator("object.simple_operator",
                             text="Print Encouraging Message")
        self.layout.prop(context.scene, 'encouraging_message')
        self.layout.prop(context.scene, 'my_int_prop')
        self.layout.prop(context.scene, 'my_float_prop')
        self.layout.prop(context.scene, 'my_bool_prop')
        self.layout.prop(context.scene, 'my_int_vector_prop')
        self.layout.prop(context.scene, 'my_float_vector_prop')
        self.layout.prop(context.scene, 'my_bool_vector_prop')


    @classmethod
    def register(cls):
        print("Registered class: %s " % cls.bl_label)
        # Register properties related to the class here.


    @classmethod
    def unregister(cls):
        print("Unregistered class: %s " % cls.bl_label)

See Table 5-4 for a list of available bpy.props.* variables. See the API documentation page for bpy.props for more information. So far we have not covered EnumProperty, CollectionProperty, or PointerProperty. We cover EnumProperty later in this chapter, and we cover CollectionProperty in Chapter 7 on advanced add-on functionalities.

Table 5-4. Available Blender Properties

BoolProperty

EnumProperty

IntProperty

StringProperty

BoolVectorProperty

FloatProperty

IntVectorProperty

 

CollectionProperty

FloatVectorProperty

PointerProperty

 

The arguments given to property declarations are generally straightforward, and many of them are shared across different properties. Most notably:

  • default= is a value or tuple of a length equal to the size that specifies the default value.

  • name= is the value that will appear in the GUI to the left of the input field.

  • description= is a character string that is displayed when the user hovers his cursor over the GUI element.

  • precision= specifies the decimal precision in the display of any float property.

  • size= specifies the size of the vector (typically of type Vector, bpy_boolean, or bpy_int) desired in any vector property.

  • subtype= specifies the desired display formatting string for a variable. Useful examples are XYZ and TRANSLATION, which will display X, Y, Z, and W ahead of your first four variables in the UI. Another notable example is subtype="COLOR", which will create an attractive color selection UI when added to a panel. See Listing 5-4 and Figure 5-4 for an example of the color subtype. Note that Blender uses a floating-point range of (0.0, 1.0) for colors. Tables 5-5 and 5-6 show the property and vector property subtypes.

    A438961_1_En_5_Fig4_HTML.jpg
    Figure 5-4. Color subtype
    Table 5-5. Available Property Subtypes

    PIXEL

    PERCENTAGE

    ANGLE

    DISTANCE

    UNSIGNED

    FACTOR

    TIME

    NONE

    Table 5-6. Available Vector Property Subtypes

    COLOR

    VELOCITY

    EULER

    XYZ

    NONE

    TRANSLATION

    ACCELERATION

    QUATERNION

    COLOR_GAMMA

     

    DIRECTION

    MATRIX

    AXISANGLE

    LAYER

     
  • min= and max= specify the extreme values that can be displayed in the GUI as well as the extreme values that can be stored in the variables.

  • softmin= and softmax= specify the minimum and maximum slider values used to display variables and scale the slider. Arbitrary values can still be inputted manually so long as they are between min and max.

  • update= accepts a function as an argument. The function is run every time the value is updated. The specified function should accept self and context as arguments regardless of where it is declared. This function is currently undocumented but fairly well-behaved.

Listing 5-4. Using the Color Subtype
bpy.types.Scene.my_color_prop = bpy.props.FloatVectorProperty(
    name="My Color Property",
    description="Returns a vector of length 4",
    default=(0.322, 1.0, 0.182, 1.0),
    min=0.0,
    max=1.0,
    subtype='COLOR',
    size=4)

Precision Selection Add-On Example

At this point in the text, we have discussed Blender Python API concepts in a sufficient capacity to start building effective add-ons. For our first real add-on, we will parameterize the ut.act.select_by_loc() function declared in Chapter 3 to enable precise group selection in Edit Mode.

Before we begin, make sure to download Chapter 3’s iteration of ut.py from http://blender.chrisconlan.com/ut.py . We will import this in our add-on. The community has used a few different protocols for managing custom imports in add-ons. We will discuss a common protocol for managing custom imports from single-level directories. In other words, we will import custom modules that lie in the same directory as the main script.

Code Overview for Our Add-On

We outline the steps taken to build the add-on, from development through deployment and sharing :

  1. Create the main script and name it __init__.py in Blender’s Text Editor. Copy the add-on template from Listing 5-1 into this script.

  2. Create a second script and name it ut.py in Blender’s Text Editor. Copy the Python module at http://blender.chrisconlan.com/ut.py into this script.

  3. Modify bl_info for our new add-on.

  4. Add the custom module import protocol. See Listing 5-5 starting at if "bpy" in locals():. Quite simply, to test whether or not we are Deployment Mode or Development Mode, we check whether or not bpy is in the current namespace.

    • If bpy is in the namespace at this point in the script, we have previously loaded the add-on and its dependent modules. In this case, reload the objects using importlib.reload().

    • If bpy is not in the namespace at this point, then we are loading the add-on for the first time. Import the module assuming it is sitting in the same directory as __init__.py in the filesystem. To import from the same directory as the main script, we use from . import custommodule.

    Note This protocol depends on import bpy coming after it in the script. If we import bpy ahead of this protocol, then bpy in locals() will always be True, rendering it useless.

    This protocol will behave nicely when the add-on has been loaded in Blender, or otherwise, when it has been deployed. We will import custom modules normally when developing in the Blender Text Editor.

  5. Import any native Blender and/or native Python modules normally.

  6. Declare our core operator class, SelectByLocation. We will parameterize ut.act.select_by_loc() with sensible inputs as scene properties.

    • Use bpy.props.FloatVectorProperty to register bounding boxes.

    • Use bpy.props.EnumProperty to register menus for selection mode and coordinate system. See Listings 3-8 through 3-10 in Chapter 3 for an explanation of these parameters.

  7. Declare our core panel class, XYZSelect. We will organize the buttons and parameters associated with operator here. The default menu layout looks pretty intuitive in this case. Declare the poll() classmethod to return True only if the mode is Edit Mode.

  8. Implement safe and verbose registration, as shown in Listing 5-1.

Listing 5-5. XYZ-Select Add-On
bl_info = {
    "name": "XYZ-Select",
    "author": "Chris Conlan",
    "location": "View3D > Tools > XYZ-Select",
    "version": (1, 0, 0),
    "blender": (2, 7, 8),
    "description": "Precision selection in Edit Mode",
    "category": "3D View"
}
### Use these imports to during development ###
import ut
import importlib importlib.reload(ut)


### Use these imports to package and ship your add-on ###
# if "bpy" in locals():
#    import importlib
#    importlib.reload(ut)
#    print('Reloaded ut.py')
# else:
#    from . import ut
#    print('Imported ut.py')


import bpy import os import random

# Simple Operator with Extra Properties

class xyzSelect(bpy.types.Operator):
    bl_idname = "object.xyz_select"
    bl_label = "Select pieces of objects in Edit Mode with bounding boxes"


    def execute(self, context):

        scn = context.scene

        output = ut.act.select_by_loc(lbound=scn.xyz_lower_bound,
                                      ubound=scn.xyz_upper_bound,
                                      select_mode=scn.xyz_selection_mode,
                                      oords=scn.xyz_coordinate_system)


        print("Selected " + str(output) + " " + scn.xyz_selection_mode + "s")

        return {'FINISHED'}

    @classmethod
    def register(cls):
        print("Registered class: %s " % cls.bl_label)
        bpy.types.Scene.xyz_lower_bound = bpy.props.FloatVectorProperty(
            name="Lower",
            description="Lower bound of selection bounding box",
            default=(0.0, 0.0, 0.0),
            subtype='XYZ',
            size=3,
            precision= 2
        )
        bpy.types.Scene.xyz_upper_bound = bpy.props.FloatVectorProperty(
            name="Upper",
            description="Upper bound of selection bounding box",
            default=(1.0, 1.0, 1.0),
            subtype='XYZ',
            size=3,
            precision=2
        )


        # Menus for EnumProperty's
        selection_modes = [
            ("VERT", "Vert", "", 1),
            ("EDGE", "Edge", "", 2),
            ("FACE", "Face", "", 3),
        ]
        bpy.types.Scene.xyz_selection_mode =
            bpy.props.EnumProperty(items=selection_modes, name="Mode")


        coordinate_system = [
            ("GLOBAL", "Global", "", 1),
            ("LOCAL", "Local", "", 2),
        ]
        bpy.types.Scene.xyz_coordinate_system =
            bpy.props.EnumProperty(items=coordinate_system, name="Coords")


    @classmethod
    def unregister(cls):
        print("Unregistered class: %s " % cls.bl_label)
        del bpy.context.scene.xyz_coordinate_system
        del bpy.context.scene.xyz_selection_mode
        del bpy.context.scene.xyz_upper_bound
        del bpy.context.scene.xyz_lower_bound


# Simple button in Tools panel
class xyzPanel(bpy.types.Panel):
    bl_space_type = "VIEW_3D"
    bl_region_type = "TOOLS"
    bl_category = "XYZ-Select"
    bl_label = "Select by Bounding Box"


    @classmethod
    def poll(self, context):
        return context.object.mode == 'EDIT'


    def draw(self, context):
        scn = context.scene
        lay = self.layout
        lay.operator('object.xyz_select', text="Select Components")
        lay.prop(scn, 'xyz_lower_bound')
        lay.prop(scn, 'xyz_upper_bound')
        lay.prop(scn, 'xyz_selection_mode')
        lay.prop(scn, 'xyz_coordinate_system')


    @classmethod
    def register(cls):
        print("Registered class: %s " % cls.bl_label)


    @classmethod
    def unregister(cls):
        print("Unregistered class: %s " % cls.bl_label)


def register():
    # bpy.utils.register_module(__name__)


    bpy.utils.register_class(xyzSelect)
    bpy.utils.register_class(xyzPanel)


    print("%s registration complete " % bl_info.get('name'))

def unregister():
    # bpy.utils.unregister_class(xyzPanel)
    # bpy.utils.unregister_class(xyzSelect)


    bpy.utils.unregister_module(__name__)
    print("%s unregister complete " % bl_info.get('name'))


if __name__ == "__main__":
    try:


        unregister()
    except Exception as e:
        print(e)
        pass


    register()

See Figure 5-5 for an example of precisely contorting an icosphere using this plugin.

A438961_1_En_5_Fig5_HTML.jpg
Figure 5-5. Color subtype

We introduced two new concepts in this example—the poll() classmethod and the EnumProperty variable. We explain these both next.

The poll() Classmethod

The poll() classmethod is a function typically placed after the bl_* variables in a panel declaration. The function will be called whenever the 3D Viewport updates to determine whether or not to display the panel.

If the function returns any non-null value, the panel will display. It is considered best practice to return a Boolean even though any non-null will suffice. Recall that the number 0, empty strings, and False are all considered null in Python.

In our add-on, we simply return True if the user is in Edit Mode, as seen here:

# poll function for edit-mode-only panels
@classmethod
def poll(self, context):
    return context.object.mode == 'EDIT'

EnumProperty Variables

The bpy.props.EnumProperty class is how we display drop-down menus via API. It is instantiated by a list of tuples, where each element in a tuple represents a Blender data value. The schema is as follows:

my_enum_list = [
    ("python_1", "display_1", "tooltip_1", "icon_1", 'number_1),
    ("python_2", "display_2", "tooltip_2", "icon_2", 'number_2),
    # etc ...
    ("python_n", "display_n", "tooltip_n", "icon_n", 'number_n)
]

This is directly from the API documentation:

  1. The first parameter is the value returned by bpy.context.scene.my_enum_list in Python.

  2. The second parameter is the value displayed in the GUI menu.

  3. The third value is the tooltip displayed in the GUI menu. It can be an empty string.

  4. (Optional) Integer or string identifier, used internally and by bpy.types.UILayout.icon.

  5. (Optional) Unique value stored in file data, used when the first parameter is potentially dynamic.

Preparing Our Add-On for Distribution

To prepare our add-on for distribution , follow these steps:

  1. Uncomment the import lines per the instructions in the comments.

  2. Revert script to explicit registration and explicit unregistration.

  3. (Optional) Remove verbose print statements when you’re done testing the add-on. This is purely to avoid cluttering the end user’s terminal.

  4. Replace the modules in the following file hierarchy and compress it as a .zip file.

xyz-select/
  |  __init__.py
    ut.py

To install our add-on, navigate to Header Menu ➤ File ➤ User Preferences ➤ Add-ons ➤ Install From File. From there, check and uncheck the box to enable and disable the add-on. This will trigger the register() and unregister() methods in __init__.py. Registration should succeed without errors.

To download the zipped add-on directly, go to http://blender.chrisconlan.com/xyz-select.zip .

Conclusion

In the next chapter, we discuss the blf and bgl modules for visualizing data in the 3D Viewport. In Chapter 7, we introduce advanced add-on development concepts.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.219.249.210