OSArch Community

[Blender] Create your first Blender add-on

  1. G

    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 :

  2. C

    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.

  3. C

    @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
    
  4. B

    @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")

  5. C

    @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 :-)

  6. C

    @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))
    
  7. C

    @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.

  8. G

    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)

  9. C

    @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

  10. G

    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

  11. C

    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)
    
  12. C

    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.

  13. C

    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'}
    
  14. G

    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.

  15. C

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

  16. G

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

  17. C

    @Gorgious said:

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

    The length should be readonly, but when the user adjust the N or Center to Center it should see the updated length immedetialy.

    I asked chatGPT, but this code is missing something because it didn't do anything:

    
    def update_length(self, context):
    
        self.my_length = self.my_n * self.my_center_to_center_distance
    
        #bpy.context.area.tag_redraw()
    
        for area in bpy.context.screen.areas:
    
            if area.type == 'VIEW_3D':
    
                area.tag_redraw()
    
    class DimensionProperties(bpy.types.PropertyGroup):
    
    
        my_n: bpy.props.IntProperty(default=1,name="N", step=1, min=2, max=100)
    
        my_height: bpy.props.FloatProperty(default=0.1, min=1, max=100, name="Height")
    
        my_center_to_center_distance: bpy.props.FloatProperty(default=0.01, min=0.01, max=100, name="Center to Center")
    
    
        my_length: bpy.props.StringProperty(name="Length", update=update_length)
    
    
        my_profile_x: bpy.props.FloatProperty(default=0.1, min=1, max=100)
    
        my_profile_y: bpy.props.FloatProperty(default=0.1, min=0.1, max=100)
    
    
  18. G

    You have to initialize the callback on my_n and my_center_to_center_distance, not my_length :

    
    def update_length(self, context):
    
        self.my_length = str(self.my_n * self.my_center_to_center_distance)  # Don't forget to cast the value since you're using a string.
    
    
    my_n: bpy.props.IntProperty(default=1,name="N", step=1, min=2, max=100, update=update_length)
    
    my_center_to_center_distance: bpy.props.FloatProperty(default=0.01, min=0.01, max=100, name="Center to Center", update=update_length)
    
    my_length: bpy.props.StringProperty(name="Length")
    

    This should work ? BTW I don't think you have to tag the UI for a redraw here, the property changing should cause the UI to redraw.

  19. C

    @Gorgious said:

    You have to initialize the callback on my_n and my_center_to_center_distance, not my_length :

    def update_length(self, context):

    self.my_length = str(self.my_n * self.my_center_to_center_distance)  # Don't forget to cast the value since you're using a string.

    my_n: bpy.props.IntProperty(default=1,name="N", step=1, min=2, max=100, update=update_length)

    my_center_to_center_distance: bpy.props.FloatProperty(default=0.01, min=0.01, max=100, name="Center to Center", update=update_length)

    my_length: bpy.props.StringProperty(name="Length")

    This should work ? BTW I don't think you have to tag the UI for a redraw here, the property changing should cause the UI to redraw.

    Thank you, it worked.

    in properties.py

    
    def update_length(self, context):
    
        self.my_length = str(self.my_n * round(self.my_center_to_center_distance,2))  # Don't forget to cast the value since you're using a string.
    
    class DimensionProperties(bpy.types.PropertyGroup):
    
    
        my_height: bpy.props.FloatProperty(default=0.1, min=1, max=100, name="Height")
    
        my_n: bpy.props.IntProperty(default=1,name="N", step=1, min=2, max=100, update=update_length)
    
        my_center_to_center_distance: bpy.props.FloatProperty(default=0.01, min=0.01, max=100, name="Center to Center", update=update_length)
    
        my_length: bpy.props.StringProperty(name="Length")
    

    in ui.py

    
    class PANEL_PT_demo(Panel):
    
        bl_label = 'Panel demo'
    
        bl_space_type = 'VIEW_3D'
    
        bl_region_type = "UI"
    
        bl_category = "Bim"
    
        #bl_region_type= 'WINDOW'
    
        #bl_context = 'render'
    
    
        def draw(self, context):
    
    
            dimension_properties = context.scene.dimension_properties
    
    
            layout = self.layout
    
    
            box = layout.box()
    
            row = box.row()
    
            row.prop(dimension_properties, 'my_height')
    
    
            box.prop(dimension_properties, "my_n")
    
            box.prop(dimension_properties, "my_center_to_center_distance")
    
            box.prop(dimension_properties,'my_length',emboss=False)
    

    Don't forget to cast the value since you're using a string.

    I have no idea what you mean by this ?

  20. G

    Hehe glad it worked :)

    Don't forget to cast the value since you're using a string.

    I have no idea what you mean by this ?

    I mean you're using a StringProperty to hold the value of my_length. It's representing a numerical value so I assume you have a reason to use a String Property instead of a FloatProperty. There are valid reasons to do so, mainly because float values are limited in precision, as you can see in your gif when your change a value the last digits of the length kind of go haywire because of that. Strings can hold an infinite precision (at least in the needed significance range for construction projects).

    Internally it is stored as a string of characters, not as a numerical value. Python is a dynamic language which lets you do a lot of things without worrying too much about the underlying data structure, but you have to be careful about some things. print("2") and print(2) will yield the same result, however print("2" + 2) will raise an error. The interpreter can't assume how you want to compute non-trivial operations. So I'd recommend you to cast the value to the intended data structure when you're dealing with dynamic data types. print("2" + str(2)) will yield "22" when print(float("2") + 2) will yield "4.0"

  21. C

    Thanks for the help, I am trying to make some experimental add-on in which all the geometry is completed IFC based. Don't have any use case but I'm just exploring possibilities of what is possible and could be practical.

  22. C

    I hope this is a very simple question, but gooling gave me unsatisfying results:

    I have the following add-on :

    The workflow should be as following:

    1. folder icon sets a path

    2. > icon adds an image in Blender, so you could make transformations to the image in Blender, e.g. scaling, rotation and position

    3. + icon saves the path images with transformation as an IfcPropertySet under IfcProject

    4. Load image(s) from IFC loads the images with transformations from the IFC.

    I have constructed the class in operator.py as following:

    
    class ImageCollectionActions(bpy.types.Operator):
    
        bl_idname = "image.collection_actions"
    
        bl_label = "Execute"
    
        action: bpy.props.EnumProperty(items=(("add",) * 3,("remove",) * 3,),)
    
    
    
        index: bpy.props.IntProperty(default=-1)
    
    
        def execute(self, context):
    
    
            image_collection = context.scene.image_collection
    
    
            if self.action == "add":        
    
                image_item =  image_collection.items.add()  
    
    
    
    
    
            if self.action == "remove":
    
                image_collection.items.remove(self.index)
    
    
            #for item in image_collection.items:
    
    
    
    
            return {"FINISHED"} 
    

    Now I have another class AddReferenceImage in which I want to get the path of the image stored .

    
    class AddReferenceImage(bpy.types.Operator):
    
        """Import Reference Image"""
    
        bl_idname = "add.referenceimage"
    
        bl_label = "Add Image"
    
    
        def execute(self, context):
    
    
            image_collection    =   context.scene.image_collection 
    

    the ui looks like this:

    
    class PANEL_PT_demo(Panel):
    
        bl_label = 'IFC Reference Images'
    
        bl_space_type = 'VIEW_3D'
    
        bl_region_type = "UI"
    
        bl_category = "IFC Reference Images"
    
    
        def draw(self, context):
    
    
            image_properties = context.scene.image_properties
    
    
            layout = self.layout
    
    
            box = layout.box()
    
            row = box.row()
    
    
            box.operator("load.referenceimage")
    
    
            layout = self.layout
    
            box = layout.box()
    
            box.operator("image.collection_actions", text="Add", icon="ADD").action = "add"
    
    
            image_collection = context.scene.image_collection
    
            row = layout.row(align=True)
    
    
            for i, item in enumerate(image_collection.items):
    
    
    
                row = box.row(align=True)
    
                row.prop(item, "image")
    
                row.operator("add.referenceimage", text="", icon="RIGHTARROW")
    
                row.operator("store.referenceimage", text="", icon="PLUS")
    
                op = row.operator("image.collection_actions", text="", icon="REMOVE")
    
                op.action = "remove"
    
                op.index = i 
    

    How can I know in AddReferenceImage class which index the user is clicking?

    Is the user clicking on the 0, 1, 2 3, etc. item of the collection?

  23. B

    I wasn't able to test right now, but would it work if you just add an index to th AddReferenceImage operator as well? And then the UI would be like:

    
     for i, item in enumerate(image_collection.items):
    
    
                row = box.row(align=True)
    
                row.prop(item, "image")
    
                op = row.operator("add.referenceimage", text="", icon="RIGHTARROW")
    
                op.index = i 
    
                row.operator("store.referenceimage", text="", icon="PLUS")
    
                op = row.operator("image.collection_actions", text="", icon="REMOVE")
    
                op.action = "remove"
    
                op.index = i 
    
  24. C

    @bruno_perdigao

    Yes thank you!

    Now I can do in operator.py this:

    
    class AddReferenceImage(bpy.types.Operator):
    
        """Import Reference Image"""
    
        bl_idname = "add.referenceimage"
    
        bl_label = "Add Image"
    
    
        index: bpy.props.IntProperty(default=-1)
    
    
        def execute(self, context):
    
    
            image_collection    =   context.scene.image_collection
    
            image_item = image_collection.items[self.index]
    
  25. C

    How would I create environments in Python Blender without python defaulting to the system installed modules?

  1. Page 1
  2. 2
  3. 3
  4. 4
  5. 5

Login or Register to reply.