[Blender] Create your first Blender add-on

13

Comments

  • edited February 2023

    You can fetch the relevant data from ifc and dynamically update the enum items with a callback. For example here's where the types are populated and here's where the dynamic enums are used to fetch the data. Remember to always have a permanent handle to the dynamic enum items or else you'll start seeing weird bugs in how they're displayed. See https://blender.stackexchange.com/questions/216230/is-there-a-workaround-for-the-known-bug-in-dynamic-enumproperty or the warning snippet in the docs for more information

    Coen
  • @Gorgious
    Thanks, very insightful answer! :-)
    Another question:
    What would be best practice for saving the settings in the Blender UI?
    For example, the end-user would like to save this configuration in the UI:

    My idea was to store the settings in a text file somewhere. Kind of similar to Navisworks Search Sets. But after struggling for a while I have realized it might not be such a good idea. Is there no native Blender functionality which is capable of doing this?

  • edited February 2023

    There are two ways to store these settings natively in Blender : inside a single file, or across all blend files. If you don't mind each file having its own set of settings, you can add them to the Scene object type as a CollectionProperty and do your thing. I would advise against it, since it will only be available in this specific file and pretty much impossible to export to another file.

    It is possible to store these settings natively across blender files using the addon preferences. here's an example how to do it. Depending on how you defined the properties you want to save, it might be as easy as using this PropertyGroup as a PointerProperty or CollectionProperty in your addons prefs and adding a custom UI to save / load it. Note : you do not have to expose the addon preferences custom properties to the user. Use a custom draw method and show only what you want.

    I've done something like that in one of my addons. I self-imposed the constraint of not having external files to store this because I don't think it would be a good experience for the user. It does make the code more complicated than it needs to.

    I think in your case it would be more straightforward to use a txt file, that way users can reliably save their preferences, and share it with other users easily. You can add all sorts of custom information in your files. You should however explore json formatting which is a reliable way to serialize / deserialize custom data. That way you don't have to design a parser from scratch. One aded benefit is that you can theoretically use these files in other platforms, you won't be dependant on Blender's implementation. You could even use a format that is universally recognized for doing such tasks, etc.

    Coentheoryshaw
  • I think in your case it would be more straightforward to use a txt file, that way users can reliably save their preferences, and share it with other users easily. You can add all sorts of custom information in your files. You should however explore json formatting which is a reliable way to serialize / deserialize custom data. That way you don't have to design a parser from scratch. One aded benefit is that you can theoretically use these files in other platforms, you won't be dependant on Blender's implementation. You could even use a format that is universally recognized for doing such tasks, etc.

    Thanks, this is a really useful tip

  • edited February 2023

    I managed to create a json file from all the Properties in my PropertyGroup class.

    {
        "my_selectionload": "C:\\Algemeen\\07_ifcopenshell\\00_ifc\\02_ifc_library\\IFC4 demo_selectionset.json",
        "my_ifcproduct": false,
        "my_ifcproductname": false,
        "my_ifcproducttypename": false,
        "my_ifcbuildingstorey": false,
        "my_ifcclassification": false,
        "my_ifcmaterial": false,
        "my_ifcpsetcommon": true,
        "my_isexternal": true,
        "my_loadbearing": true,
        "my_firerating": false,
        "my_acousticrating": false,
        "my_length": false,
        "my_width": true,
        "my_height": true,
        "my_area": false,
        "my_netarea": false,
        "my_netsidearea": false,
        "my_grossarea": false,
        "my_grosssidearea": false,
        "my_volume": true,
        "my_netvolume": true,
        "my_grossvolume": true,
        "my_spreadsheetfile": "C:\\Algemeen\\07_ifcopenshell\\00_ifc\\02_ifc_library\\IFC Schependomlaan_blenderbim.xlsx",
        "ods_or_xlsx": "ODS",
        "key1": "1",
        "key2": "2"
    }
    

    How would I set all the properties accordingly to this json file in the Blender UI? I am struggling with the logic.
    So far I came up with this:

            ifc_properties = context.scene.ifc_properties
            selection_file = open(ifc_properties.my_selectionload)
            selection_configuration = json.load(selection_file)
    
            for my_ifcproperty in ifc_properties.__annotations__.keys():
                my_ifcpropertyvalue = getattr(ifc_properties, my_ifcproperty)
    
                for property_name_from_json, property_value_from_json in selection_configuration.items():
                    if my_ifcproperty==property_name_from_json:
    
                        if property_value_from_json == True:
                            ifc_properties.my_ifcproductname = True
    
    
                        if property_value_from_json == False:
                            ifc_properties.my_ifcproductname = False
    

    But sometimes it seems to work and other times not, I am getting a bit confused.

  • edited February 2023

    I will try to answer on a practical standpoint for your last few lines, they can be considered an anti-pattern. First and foremost, in python you don't compare to boolean values with the equality symbol. You use if a is True, not if a == True. I won't go into technical details but you can look it up if you want. It can be reduced to ifc_properties.my_ifcproductname = bool(property_value_from_json), but I would use another method.

    I also don't know where ifc_properties.my_ifcproductname comes from ?

    I think you can reduce this whole for loop with just :

    for property_name_from_json, property_value_from_json in selection_configuration.items():
        if not hasattr(ifc_properties, property_name_from_json):
            continue  # don't bother if we can't reliably copy the property
        setattr(ifc_properties, property_name_from_json, property_value_from_json)
    

    You may need to cast the values if you don't want blender to complain, since json is formatting everything to a string format (except boolean values I guess ?).

    my_property_type = type(getattr(ifc_properties, property_name_from_json))
    setattr(ifc_properties, property_name_from_json, my_property_type (property_value_from_json))
    
    theoryshawCoen
  • Just dropping in to say this thread is a goldmine of knowledge and a huge thanks to @Gorgious for being so generous with his knowledge!

    theoryshawGorgiousCoen
  • Hehe thank you it means a lot. I feel like I'm just doing my part in giving back a part of what was given to me by others :)

    Coen
  • @Gorgious
    Thank you so much! I did not know about the setattr function.
    Found here a good post when to use is and when to use ==

    Gorgious
  • @Gorgious said:
    You can fetch the relevant data from ifc and dynamically update the enum items with a callback. For example here's where the types are populated and here's where the dynamic enums are used to fetch the data. Remember to always have a permanent handle to the dynamic enum items or else you'll start seeing weird bugs in how they're displayed. See https://blender.stackexchange.com/questions/216230/is-there-a-workaround-for-the-known-bug-in-dynamic-enumproperty or the warning snippet in the docs for more information

    I'm really struggling with this, I want to update the EnumProperty from a Json file.
    So far I have this:

    class CustomCollectionActions(bpy.types.Operator):
        bl_idname = "custom.collection_actions"
        bl_label = "Execute"
        action: bpy.props.EnumProperty(
            items=(
                ("add",) * 3,
                ("remove",) * 3,
            ),
        )
        def execute(self, context):
    
            custom_collection = context.scene.custom_collection
            if self.action == "add":           
                item = custom_collection.items.add()  
            if self.action == "remove":
                custom_collection.items.remove(len(custom_collection.items) - 1 )
            return {"FINISHED"}  
    
        def set_configuration(context, property_set, property_name):
    
            print ('set configruaton method',property_set, property_name) 
    
            return {"FINISHED"}  
    

    The set configuration method is being called in another class ConfirmSelection like so:

    for property_name_from_json, property_value_from_json in selection_configuration.items():
                if property_name_from_json.startswith('my_ifccustomproperty'):
                    set_configuration(context, property_set=property_name_from_json, property_name=property_value_from_json)
    

    A section of the json file looks like this:

    "my_ifccustomproperty1": "1",
        "my_ifccustomproperty2": "3",
        "my_ifccustomproperty3": "3",
        "my_ifccustomproperty4": "4",
        "my_ifccustomproperty5": "5"
    

    I think I need to update action: bpy.props.EnumProperty. but before I can update it with a value I need to add an empty item?

  • @Coen said:

    @Gorgious said:
    You can fetch the relevant data from ifc and dynamically update the enum items with a callback. For example here's where the types are populated and here's where the dynamic enums are used to fetch the data. Remember to always have a permanent handle to the dynamic enum items or else you'll start seeing weird bugs in how they're displayed. See https://blender.stackexchange.com/questions/216230/is-there-a-workaround-for-the-known-bug-in-dynamic-enumproperty or the warning snippet in the docs for more information

    I'm really struggling with this, I want to update the EnumProperty from a Json file.
    So far I have this:

    class CustomCollectionActions(bpy.types.Operator):
        bl_idname = "custom.collection_actions"
        bl_label = "Execute"
        action: bpy.props.EnumProperty(
            items=(
                ("add",) * 3,
                ("remove",) * 3,
            ),
        )
        def execute(self, context):
    
            custom_collection = context.scene.custom_collection
            if self.action == "add":           
                item = custom_collection.items.add()  
            if self.action == "remove":
                custom_collection.items.remove(len(custom_collection.items) - 1 )
            return {"FINISHED"}  
    
        def set_configuration(context, property_set, property_name):
          
            print ('set configruaton method',property_set, property_name) 
    
            return {"FINISHED"}  
    

    The set configuration method is being called in another class ConfirmSelection like so:

    for property_name_from_json, property_value_from_json in selection_configuration.items():
                if property_name_from_json.startswith('my_ifccustomproperty'):
                    set_configuration(context, property_set=property_name_from_json, property_name=property_value_from_json)
    

    A section of the json file looks like this:

    "my_ifccustomproperty1": "1",
        "my_ifccustomproperty2": "3",
        "my_ifccustomproperty3": "3",
        "my_ifccustomproperty4": "4",
        "my_ifccustomproperty5": "5"
    

    I think I need to update action: bpy.props.EnumProperty. but before I can update it with a value I need to add an empty item?

    I was overthinking this, it was suprisingly easy:

        def set_configuration(context, property_set, property_name):
            custom_collection = context.scene.custom_collection
            custom_collection.items.add().name = property_name
    
            return {"FINISHED"}   
    
  • How would I clear an enumproperty?
    Found this
    custom_collection.items.clear but I don't see it doing anything.

  • @Coen said:
    How would I clear an enumproperty?
    Found this
    custom_collection.items.clear but I don't see it doing anything.

    Disregard, I spoke too soon, the method is going through a loop so custom_collection.items.remove(1) was sufficient. Thank you for reading.

  • The result, you can now load a json file and will store the settings:

    theoryshawGorgiousAce
  • edited March 2023

    Hehe looks like my work here is done, you're ready to troubleshoot your addon on your own :)

    Just kidding, don't hesitate to post questions here if need be ;)
    BTW custom_collection.items.clear doesn't call the method, you have to use custom_collection.items.clear()

    I personally think it's better UX wise to let the user delete specific items from a collection. You could modify your operator a bit by adding an index input

    class CustomCollectionActions(bpy.types.Operator):
        bl_idname = "custom.collection_actions"
        bl_label = "Execute"
        action: bpy.props.EnumProperty(
            items=(
                ("add",) * 3,
                ("remove",) * 3,
            ),
        )
        index: bpy.props.IntProperty(default=-1)
    
     def execute(self, context):
    
            custom_collection = context.scene.custom_collection
            if self.action == "add":           
                item = custom_collection.items.add()  
            if self.action == "remove":
                if self.index < 0:
                    custom_collection.items.remove(len(custom_collection.items) - 1 )
                else:
                    custom_collection.items.remove(index) # Be careful here we're not checking if this is a valid index, might throw an error
            return {"FINISHED"}  
    

    and in your panel draw method

    def draw(self, context):
        # a bunch of things
        box = self.layout.box()
        for i, item in enumerate(context.scene.custom_collection.items):
            op = box.operator("custom.collection_actions")
            op.action = "remove"
            op.index = i
        box.operator("custom.collection_actions").action = "add"
    

    or something like that (not tested)

    Here's an example of how I used a similar concept in a fork of the prj addon :

    theoryshawCoenAce
  • edited March 2023

    Quite simple question:

    What would be best practice to open an external file from Blender using python?
    I've read the documentation here:
    https://docs.blender.org/api/current/bpy.ops.file.html
    But there doesn't seem to be an operator to open a file, while in the UI there is the possibility to click on the folder icon:

    When I look in the console and click the folder icon it gives no output. While it opens a file.
    I could use the os module of python, but I have no way of testing it on other operating systems except windows.
    That's why I am researching if it's possible to do it with Blender.

  • edited March 2023

    @Gorgious

    index: bpy.props.IntProperty(default=-1)

    Don't really understand why the default should be -1? Doesn't that just remove the last item from the list?
    I thought the index should be the specific item in the list the user wants to remove from the propertyset list?

    EDIT: should have read your example

    box = col.box()
     column = box.column(align=True)
    row = column.row(align=True)
    row.prop(settings, "back_subjects_collections", text="Back subjects collections")
    op = row.operator("prj.container_collection_add_or_remove", icon="ADD", text="")
    op.operation = "ADD"
    for i, container in enumerate(settings.back_subjects_collections):
            row = column.row()
            row.prop(container, "collection", text=f"Collection {i + 1}")
            op = row.operator("prj.container_collection_add_or_remove", icon="REMOVE", text="")
            op.operation = "REMOVE"
            op.idx = i
    
  • @Coen said:

    What would be best practice to open an external file from Blender using python?

    bpy.ops.bim.load_project(filepath="/path/to/file.ifc")

    Coen
  • @brunopostle said:

    @Coen said:

    What would be best practice to open an external file from Blender using python?

    bpy.ops.bim.load_project(filepath="/path/to/file.ifc")

    I meant more like opening an external document on your native os, like a text or xml file from Blender, not an ifc file. But thanks :-)

  • @Coen said:

    @brunopostle said:

    @Coen said:

    What would be best practice to open an external file from Blender using python?

    bpy.ops.bim.load_project(filepath="/path/to/file.ifc")

    I meant more like opening an external document on your native os, like a text or xml file from Blender, not an ifc file. But thanks :-)

    Found it in this SO post

    import subprocess, os, platform
    if platform.system() == 'Darwin':       # macOS
        subprocess.call(('open', filepath))
    elif platform.system() == 'Windows':    # Windows
        os.startfile(filepath)
    else:                                   # linux variants
        subprocess.call(('xdg-open', filepath))
    
    brunopostle
  • @Gorgious

    Just kidding, don't hesitate to post questions here if need be ;)

    I thought I understood the code, but I am missing something. Which I think is reasonably simple.
    What I have at the moment:

    What I like to achieve:

    At the moment this is the code:
    in prop.py

    class CustomItem(bpy.types.PropertyGroup):
        name: bpy.props.StringProperty(name         ="Property",
                                             description  ="Use the PropertySet name and Property name divided by a .",
                                       default      ="PropertySet.Property"
                                       )
    
    class CustomCollection(bpy.types.PropertyGroup):
        items: bpy.props.CollectionProperty(type=CustomItem) 
    

    in ui.py

    class CUSTOM_PROPERTIES_IFC_PT_PANEL(GENERAL_panel, Panel):
        bl_parent_id = "EXAMPLE_PT_panel_1"
        bl_label = "Custom Properties"
    
        def draw(self, context):
    
            layout = self.layout
            box = layout.box()
            custom_collection = context.scene.custom_collection
            row = layout.row(align=True)
            row.operator("custom.collection_actions", text="Add", icon="ADD").action = "add"
            row.operator("custom.collection_actions", text="Remove Last", icon="REMOVE").action = "remove"
    
            for i, item in enumerate(custom_collection.items):
                box.prop(item, "name")
            row = layout.row(align=True) 
    

    in operator.py:

    class CustomCollectionActions(bpy.types.Operator):
        bl_idname = "custom.collection_actions"
        bl_label = "Execute"
        action: bpy.props.EnumProperty(items=(("add",) * 3,("remove",) * 3,),)
        #index: bpy.props.IntProperty(default=-1)
    
        def execute(self, context):
    
            custom_collection = context.scene.custom_collection
    
            if self.action == "add":        
                custom_item = custom_collection.items.add()  
    
            if self.action == "remove":
                custom_collection.items.remove(len(custom_collection.items) - 1 )
    
    
            return {"FINISHED"}
    

    I understand the enumerated i is the index which can be used to delete a certain item from the list, but I am struggling to create a delete button at the end of the box after the property as I have drawn with red rectangles.

  • The UI layout is created by gluing UI elements together. If you use prop, it will usually add a single row with the property as a field. If you want to use several properties on the same row, you need to use a custom row. Actually the same things as what you did with the add and delete operators.

            for i, item in enumerate(custom_collection.items):
                row = box.row(align=True)
                row .prop(item, "name")
                op = row.operator("custom.collection_actions", text="", icon="REMOVE")
                op.action = "remove"
                op.index = i
    

    should do the trick (not tested)

    Coen
  • edited March 2023

    @Gorgious said:
    The UI layout is created by gluing UI elements together. If you use prop, it will usually add a single row with the property as a field. If you want to use several properties on the same row, you need to use a custom row. Actually the same things as what you did with the add and delete operators.

            for i, item in enumerate(custom_collection.items):
                row = box.row(align=True)
                row .prop(item, "name")
                op = row.operator("custom.collection_actions", text="", icon="REMOVE")
                op.action = "remove"
                op.index = i
    

    should do the trick (not tested)

    oh wow that was easy XD

  • I like for the Add button to be on top of the list, that way it doesn't move when you add new elements. It can be annoying when you want to add several items at a time

    Coen
  • I like for the Add button to be on top of the list, that way it doesn't move when you add new elements. It can be annoying when you want to add several items at a time

    This is a very good point, thanks for your help and fast reply once again @Gorgious . To recap I eventually made this:

    in operator.py

    class CustomCollectionActions(bpy.types.Operator):
        bl_idname = "custom.collection_actions"
        bl_label = "Execute"
        action: bpy.props.EnumProperty(items=(("add",) * 3,("remove",) * 3,),)
        index: bpy.props.IntProperty(default=-1)
    
        def execute(self, context):
    
            custom_collection = context.scene.custom_collection
    
            if self.action == "add":        
                custom_item = custom_collection.items.add()  
    
            if self.action == "remove":
                custom_collection.items.remove(self.index)
    
            return {"FINISHED"}  
    

    in ui.py

    class CUSTOM_PROPERTIES_IFC_PT_PANEL(GENERAL_panel, Panel):
        bl_parent_id = "EXAMPLE_PT_panel_1"
        bl_label = "Custom Properties"
    
        def draw(self, context):
    
            layout = self.layout
            box = layout.box()
            box.operator("custom.collection_actions", text="Add", icon="ADD").action = "add"
    
            custom_collection = context.scene.custom_collection
            row = layout.row(align=True)
            #row.operator("custom.collection_actions", text="Add", icon="ADD").action = "add"
    
            for i, item in enumerate(custom_collection.items):
                row = box.row(align=True)
                row .prop(item, "name")
                op = row.operator("custom.collection_actions", text="", icon="REMOVE")
                op.action = "remove"
                op.index = i
    
            row = layout.row(align=True)
    
  • And made this to prevent the dataframe from making columns twice if the user accidently adds the same property:

            for custom_property in custom_collection.items:
                custom_propertyset_list.append(custom_property.name)
            custom_property_unique_list = []
    
            seen = set()
            for item in custom_propertyset_list:
                if item not in seen:
                    seen.add(item)
                    custom_property_unique_list.append(item)
    

    This wil keep order of the set as well.

    Gorgious
  • edited March 2023

    I would like to add a user dialog for the user to close their spreadsheet file when it's already opened. Similar like this:

    I tried several python scripts to see if an instance of an application is running on a specific OS. But this became really complex and I abandoned the idea. Now I just check if there is a value in this field so I can assume the user has a spreadsheet opened:

    When there is a value in this field and they click 'Create Spreadsheet' again I would like to have a dialog pop up box which says. "Please close your spreadsheet first at C:\my_spreadsheetfile" .
    I googled it and found this SO post

    I've added and registered the classes from this example in my operator.py and ui.py.
    Only I am bit confused on how to call this class when this condition is met I described:

    This is the code:

    class ExportToSpreadSheet(bpy.types.Operator):
        bl_idname = "export.tospreadsheet"
        bl_label = "Create spreadsheet"
    
        def execute(self, context):
    
            ifc_properties = context.scene.ifc_properties
    
            if len(ifc_properties.my_spreadsheetfile) == 0:
                self.create_spreadsheet(context)
    
            if len(ifc_properties.my_spreadsheetfile) > 1:
    
                if self.get_current_ui_settings(context) == self.get_stored_ui_settings():
                    self.open_file_on_each_os(spreadsheet_filepath=ifc_properties.my_spreadsheetfile)
    
                if self.get_current_ui_settings(context) != self.get_stored_ui_settings():
                    if (self.check_if_file_is_open(spreadsheet_filepath=ifc_properties.my_spreadsheetfile)):
                        print ("Please close the spreadsheet file first")
                        #CALL THE CLASS HERE WHICH GIVES THE POP UP DIALOG
    
                    else:
                        self.create_spreadsheet(context)
    
            return {'FINISHED'}
    
  • edited March 2023

    Alright, this is one instance where if I understand correctly you might be overengineering your program a bit.

    Programs should be self contained and not rely too much on external programs or variable user input, to reduce bugs, technical debt and.. erm... user error :)

    If you're beginning to chain if statements you might be going too deep into the user's brain.

    You can try to go the other route, don't ask for permission, ask for forgiveness. Try to save the file, if it's not writable (because there can be several reasons why, for example the user doesn't have permission to write in this folder or in this network drive, or the folder you're trying to write to doesn't exist) catch the Error in a try / except, and if there is an error, do your thing. That way you don't have to rely on OS specific ways to check if a file is open, or if the user has permission, etc. My 2 cents.

    Also, IMO it is reasonable to expect the user to close the file they're trying to overwrite on their own if they have it open.

    Coen

  • How would I update the Length String property in real time with N x Center to Center multiplication?

  • Do you want to be able to modify either of the three parameters and the other two update or Length can be readonly ?

Sign In or Register to comment.