OSArch Community

BlenderBIM spreadsheet writer for .xlsx and .ods

  1. C

    @Gorgious

    Thank you so much! :-D the whole script now looks a lot cleaner and more efficient using defaultdict. No need to make a new list each time. Just committed. And now the add custom property button also works. :-D

    @vpajic

    How would I start integrating this with IFCCSV?, I have no idea where to start..

  2. V

    @Coen - I would simply start by looking at the general structure of the modules. You generally have 4 files:

    • init.py

    • operators.py

    • ui.py

    • prop.py

    If you have a lot of helper functions you could also create a helper.py file and import it where necessary. I only glimpsed at your code but I saw the "WriteToXLSX" operator class and thought wow, that's a behemoth ?. Perhaps start by refactoring your script so it fits in with the other modules and then simplifying the giant classes a bit? @Gorgious might be able to give you better tips though, these are just my quick 2 cents.

  3. M

    @Coen there are two aspects to merge. The first aspect has got nothing to do with the BlenderBIM Add-on - it's to do with IfcCSV directly. First, I'd get familiar with how to use IfcCSV purely through Python, a CLI, or anything that doesn't require the Blender interface. IfcCSV currently only works with CSV right now. the IfcCSV class has an import function and an export function which does the actual reading and writing to and from CSV. This needs to be split out into its own function, and have two classes - one class would do CSV, and another would do XLSX. In the future, we'd also add ODS support. You'd do this refactoring and add a new XLSXWriter/Reader class and function purely in IfcCSV land. This way, any other app in the future can benefit from it: Blender, FreeCAD, or anything at all.

    The second aspect would be the Blender interface that you've written. I think after you've done the first aspect and are familiar with how IfcCSV by itself works without Blender, the second aspect of integrating your UI should be much more straightforward, and you can look at the bim/module/csv/* directory for some hints.

    Hope it helps.

  4. C

    I've done quite some refactoring . The whole script is now only 600 lines with some more efficient use of functions.

    • The IsExternal, LoadBearing, Firerating functions is now one function called get_common_properties. I fixed a big mistake. It didn't export the loadbearing at all..

    • All the various get_quantities functions is now one function called get_quantities. If the user ticks "Area" it exports all the area defintions available like NetSideArea, GrossSideArea. Maybe I should split this up in a seperate UI to prevent confusion.

    • Removed all the global variables definitions

    My plan is start integrating the add-on into BlenderBIM when I have the export to .ods export ready. Else I don't think it's worth of the term open source.

  5. V

    @Coen I'm inspired by the effort you put into this!

  6. N

    @vpajic said:

    @Coen I'm inspired by the effort you put into this!

    Me too, Coen I can't wait for your StairMaker ?

  7. C

    I managed to write a table to an ods format with the following code:

    
            df = pd.DataFrame(ifc_dictionary)
    
            writer_ods = pd.ExcelWriter(blenderbim_openoffice_xml_properties.my_ods_file, engine='odf')
    
            df.to_excel(writer_ods, sheet_name=blenderbim_openoffice_xml_properties.my_workbook, startrow=0, header=True, index=False)
    
    
    
            worksheet_ods = writer_ods.sheets[blenderbim_openoffice_xml_properties.my_workbook]
    
            writer_ods.save()
    
            os.startfile(blenderbim_openoffice_xml_properties.my_ods_file)
    

    The result in LibreOffice:

    Now it starts to get difficult,

    I want to apply an autofilter in LibreOffice through Python.

    I found this SO post

    It seems to overcomplicate the script a lot.

    Maybe this is the wrong platform to ask, I should ask on SO.

    But I also get confused about the .osd format. Because when I open it in notepad++ I just see binary text... I expected a .xml file.

  8. B

    Thanks for all of your work on this.

    Wikipedia says .ods is a zipped xml and when I unzipped one I do see a content.xml as well as a bunch of other files.

  9. C

    @baswein

    I unzipped it and indeed see huge xml files, just wondering if I can add an autofilter directly with lxml python instead of applying an autofilter directly on libreoffice.

  10. M

    The XML schema of ODS is quite tricky to understand I find. I find that a good way to discover how things work is to create a small tiny file in Libreoffice, make a change (e.g. add your autofilter), then save it out again, then compare using diff what happened in the XMLs.

  11. C

    How would I move a variable, in my case a pandas dataframe from one class to another class in Blender?

    I made two new classes to Write to .ods and open and .ods file.

    I would like to sent the pandas dataframe to that class. With Blender is all seems to work a bit different then I am used to with Python.

  12. G

    Maybe an XY problem, you may be more at ease if you uncouple your pandas logic from both classes and only write to a particular file at the end ?

  13. C

    @Gorgious said:

    Maybe an XY problem, you may be more at ease if you uncouple your pandas logic from both classes and only write to a particular file at the end ?

    Thanks for your answer.

    I will try to be more specific. my aim is to have three classes

    • ConstructPandasDataFrame

    • WriteToXLSX

    • WriteToODS

    The reason I want the ConstructPandasDataFrame as a seperate class is I think it will help to be sofware-agnostic.

    In the ConstructPandasDataFrame class I end up with a variable called df. This variable contains the dataframe and all the data to be written to .ods or .xlslx.

    Now I made a simple class called WriteToODS

    
    class WriteToODS(bpy.types.Operator):
    
        """Write IFC data to .ods"""
    
        bl_idname = "object.write_to_ods"
    
        bl_label = "Simple ODS Object Operator"
    
    
    
        def execute(self, context):
    
            print ('hello from ods')
    
            return {'FINISHED'}
    

    In the ConstructPandasDataFrame class the dataframe is constructed, I will not copy paste it for the sake of brevity

    But in short

    
    class ConstructPandasDataFrame(bpy.types.Operator):
    
        """Construct Pandas Datafra,"""
    
        bl_idname = "object.construct_pandas_dataframe"
    
        bl_label = "Simple Pandas Object Operator"
    
    
    
        def execute(self, context):
    
            #a lot of stuff happening here but eventually I end up with
    
    
           df = pd.DataFrame(ifc_dictionary)
    
    
            return {'FINISHED'}
    

    Now I don't understand how to sent/use the variable df in the WriteToODS class

  14. G

    Alright I understand now. So you want to have a button that is only responsible for building the data frame in memory ? Couldn't it be made at runtime when the user clicks on either the button to create the xls or the ods ?

  15. C

    @Gorgious said:

    Alright I understand now. So you want to have a button that is only responsible for building the data frame in memory ? Couldn't it be made at runtime when the user clicks on either the button to create the xls or the ods ?

    No, I don't want a button for building a dataframe, I want the dataframe constructed when the xlsx or ods button is clicked :-)

  16. G

    Oh right then you don't need to inherit from bpy.types.Operator, you can even keep it as a function even though it could benefit from being object oriented. Then in your WriteToODS execute you go df = get_dataframe() with for instance :

    
    def get_dataframe()
    
        # ... logic
    
        # return pd.DataFrame(ifc_dictionary)
    

    Or if you want to use a class (easier to extend in the future and to separate concerns since you may want to break down your mega function at some point)

    
    class ConstructPandasDataFrame:
    
        def __init__(self):
    
            # ... logic
    
            self.dataframe = pd.DataFrame(ifc_dictionary)
    

    and in the WriteToODS execute :

    
    def execute(self, context):
    
        df_creator = ConstructPandasDataFrame()
    
        df = df_creator.dataframe
    
        # ... logic
    
        return {"FINISHED"}
    
  17. C

    Thanks, one thing which is not clear for me.

    in the class

    
    class ConstructPandasDataFrame:
    
        def __init__(self):
    
            # ... logic
    
            self.dataframe = pd.DataFrame(ifc_dictionary)
    

    How do I acces the context variable? I want to use the variables

    
    blenderbim_openoffice_xml_properties = context.scene.blenderbim_openoffice_xml_properties
    
    my_collection = context.scene.my_collection
    

    in the logic

  18. G

    You can pass any variable you want to the class constructor :

    
    class ConstructPandasDataFrame:
    
        def __init__(self, context):
    
            # ... logic (now you can use the provided context)
    
            self.dataframe = pd.DataFrame(ifc_dictionary)
    

    Then

    
    def execute(self, context):
    
        dataframe_creator = ConstructPandasDataFrame(context)
    
        df = dataframe_creator.dataframe
    

    Although I would try and uncouple the dataframe creation more from Blender's shenanigans so you could do :

    
    class ConstructPandasDataFrame:
    
        def __init__(self, xml_properties, my_collection):
    
            # ... logic (now you can use xml_properties and my_collection)
    
            self.dataframe = pd.DataFrame(ifc_dictionary)
    

    Then

    
    def execute(self, context):
    
        dataframe_creator = ConstructPandasDataFrame(
    
            xml_properties=context.scene.blenderbim_openoffice_xml_properties,
    
            my_collection=context.scene.my_collection
    
        )
    
        df = dataframe_creator.dataframe
    

    And you could even uncouple it more with a bit of work. That way for instance if at any one point you decide you're not going to store your things into a CollectionProperty on the scene but rather get it from an external json or csv file, you don't have to even touch the ConstructPandasDataFrame class to modify the behaviour.

    Also be aware that if at any point you can't get a handle on the current context, you can always use bpy.context which theoretically should point to the current context. Although it does not always do and the error it produces are very cryptic to say the least, so the best is to avoid using it if you have it handy from somewhere else.

  19. D

    @Coen I hope you don't mind but I've renamed this discussion to better reflect where it's gone. Let me know if you want something else.

  20. C

    @Gorgious

    on the scene but rather get it from an external json or csv file,

    Thanks for the elaborate answer, I think I am going down this route. I am so confused now. I don't understand anymore..Code also became a complete mess haha. I also still need to figure out how to apply autofilter in LibreOffice and save a filtered .ods file and read it back in Blender.

    @duncan

    That's indeed a better title.

  21. C

    @Gorgious

    Although I would try and uncouple the dataframe creation more from Blender's shenanigans so you could do :

    https://blender.stackexchange.com/questions/191098/blender-python-how-to-call-a-class-or-function

    I found this unanswered question on SO, I have class now I called:

    
    class ConstructDataFrame(bpy.types.Operator):
    
    
    
        """Construct Dataframe"""
    
        bl_idname = "object.construct_dataframe"
    
        bl_label = "Object Operator Construct Dataframe"
    
    
    
        def execute(self, context):
    
            print("Construct DataFrame")
    
           """ Does a lot of things like checking which buttons are checked to construct the dictionary"""
    
            df = pd.DataFrame(ifc_dictionary)
    
            df.to_csv(IfcStore.path.replace('.ifc','_blenderbim.csv'),sep=';')
    
    
    
            print (IfcStore.path.replace('.ifc','_blenderbim.csv'))
    
    
        return {'FINISHED'}
    

    This class should be called in other class, should I register and unregister this class? Because when I try to call this class I do the following:

    
    class WriteToXLSX(bpy.types.Operator):
    
        """Write IFC data to .xlsx"""
    
        bl_idname = "object.write_to_xlsx"
    
        bl_label = "Simple Object Operator"
    
    
    
        def execute(self, context):
    
            print("Write to .xlsx")
    
    
    
            construct_data_frame = ConstructDataFrame(context)
    
    
    
            #construct_data_frame.write_to_csv
    
    
    
            for i in dir(construct_data_frame):
    
                print (i)
    
    

    I can see it finds all the methods with dir, but no csv is made.

    When I try

    construct_data_frame.execute or any other method

    I get this error

    
    location: <unknown location>:-1
    
    Error: Python: Traceback (most recent call last):
    
      File "\BlenderBIMOpenOfficeXML.py", line 345, in execute
    
        if not custom_pset_list:
    
      File "C:\Program Files\Blender Foundation\Blender 3.0\3.0\scripts\modules\bpy_types.py", line 734, in __getattribute__
    
        properties = StructRNA.path_resolve(self, "properties")
    
    ValueError: Context.path_resolve("properties") could not be resolved
    
    
    location: <unknown location>:-1
    
  22. G

    Haha welcome to the wonderful world of programming then ! This problem is the bane of all programs that started out as making one thing well, then were expanded to make two things, and so on. Unless you spend some time reflecting a bit and refactoring to be more expandable you won't be able to add functionality as easily I'm afraid :)

  23. C

    Unless you spend some time reflecting a bit and refactoring to be more expandable you won't be able to add functionality as easily I'm afraid :)

    This is probably the best advise, I'm just impatient with myself. :-)

  24. G

    Ups it was meant to follow your previous comment ^^

    The thing here is unless you want your ConstructDataFrame class to be executed when you click on a button in the Blender interface, you do not need (nor want) it to inherit from bpy.types.Operator and implement all the associated boilerplate code. All the code that inherits or references bpy.types is basically a bridge (or interface) to the actual C codebase, and as such you can't define or access classes and objects normally as you would with regular python objects.

    You can for instance replace your code with :

    
    class ConstructDataFrame:
    
        def __init__(self, context):  # This is a python class constructor. It's executed when you do construct_data_frame = ConstructDataFrame(context)
    
            print("Construct DataFrame")
    
           """ Does a lot of things like checking which buttons are checked to construct the dictionary"""
    
            df = pd.DataFrame(ifc_dictionary)
    
            self.df = df
    
    
    class DataFrameWriter:
    
        def __init__(self, dataframe):
    
            self.df = dataframe
    
        def to_csv(self, path):
    
            self.df.to_csv(path)
    
        def to_xlsx(self, path):
    
            self.df.to_xlsx(path)  # not sure about the syntax here
    
    
    class WriteToXLSX(bpy.types.Operator):
    
        """Write IFC data to .xlsx"""
    
        bl_idname = "object.write_to_xlsx"
    
        bl_label = "Simple Object Operator"
    
    
        def execute(self, context):
    
            print("Write to .xlsx")
    
    
            construct_data_frame = ConstructDataFrame(context)
    
            writer = DataFrameWriter(construct_data_frame .df)
    
            # writer.to_csv(IfcStore.path.replace('.ifc','_blenderbim.csv'),sep=';')
    
            writer.to_xlsx(IfcStore.path.replace('.ifc','_blenderbim.xlsx'),sep=';')
    

    This is of course just a suggestion in order to show how it would work, you can do all of that with simple functions too.

    As for the initial question linked to BSE, the way you execute an operator in code is with bpy.ops and then the operator's bl_idname property eg you would call your WriteToXLSX operator with bpy.ops.object.write_to_xlsx() (solution is given in the comments). Remember this is an API, meaning a lot of python rules are bended and there is a Deus Ex Machina under the hood that's tying everything up, calling things automatically, etc.

  25. C

    @Gorgious

    After struggling the whole day I finally managed to make it work :-D. Will commit later when I have all the mess cleaned.

    EDIT: Forgot to thank you in this post :-D @Gorgious

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

Login or Register to reply.