ifcopenshell scripting on IFC file loaded in BlenderBIM

edited May 2021 in General

Hi!
I just started to explore BlenderBIM and since I already have some scripting experience with IfcOpenShell in Python, I was looking if I could directly access the IFC file(s) loaded in Blender through BlenderBIM. I'm able to load any IFC file from my drive using IfcOpenShell in the Blender scripting environment, but I want to access the currently loaded IFC file(s) in Blender to be able to combine the UI from BlenderBIM and scripting whenever needed.

Best regards,

Mathias

Comments

  • edited March 2021

    This should get you started. BTW, you may be interested in the brand new ifcopenshell.api namespace - everything you can do in the BlenderBIM Add-on is now part of IfcOpenShell itself.

    from blenderbim.bim.ifc import IfcStore
    f = IfcStore.get_file()
    
  • thanks! I'm now able to read and write the IFC data directly from the file loaded with BlenderBIM :)
    When I changed a property for testing, I could not see the change being propagated into the BlenderBIM GUI (but it's there when I export to IFC using the GUI). I believe I need to perform some kind of syncing between BlenderBIM and Blender? Is this the "authoring" mode from https://github.com/IfcOpenShell/IfcOpenShell/issues/1360#issuecomment-793351042? If so, how can I activate it? :)

  • The authoring mode handles syncing of visual operations only, but not when you're changing stuff via scripts.

    When you're changing stuff via scripts, you need to decide for yourself when to refresh data for the BlenderBIM Add-on GUI. The data is cached in data classes (currently refactored into ifcopenshell.api namespace). You can either refresh data, or just purge the existing data cache, and the UI will detect the lack of a cache and fetch anew. If you're not running master, the data classes are something like blenderbim.bim.module.foo.data. They have load() and purge() functions which you can call.

  • edited March 2021

    hmm, I not able to track down the correct data cache (BlenderBIM v210221). Can you provide an example? I loaded the IFC in the script from Blender and used IfcOpenShell to change a property:

    import ifcopenshell
    import blenderbim
    f = blenderbim.bim.ifc.IfcStore.get_file()
    wallX = f.by_guid( '2WW_2E6Wr1XPPZ0X1qKYGa' )
    for relDefinesByProperties in wallX.IsDefinedBy:
        print( relDefinesByProperties.RelatingPropertyDefinition.Name )
        if relDefinesByProperties.RelatingPropertyDefinition.Name == 'Pset_WallCommon':
            for prop in relDefinesByProperties.RelatingPropertyDefinition.HasProperties:
                print( '\t OLD: ' + prop.Name + ' = ' + str( prop.NominalValue ) )
                if prop.Name == 'LoadBearing':
                    prop.NominalValue.wrappedValue = True
                    print( '\t NEW: ' + prop.Name + ' = ' + str( prop.NominalValue ) )
    
  • edited March 2021

    Note: the namespace changes in the master to ifcopenshell.api. Hopefully this helps:

    from blenderbim.bim.module.pset.data import Data
    Data.purge() # Option 1
    Data.load(f, wallX.id()) # Option 2
    

    You may also prefer to use the data loader to get data instead of writing lower level ifcopenshell. It may also be useful to use the usecases directly for editing. For example:

    Data.load(f, wallX.id()) 
    for pset_id in Data.products.get(wallX.id(), {'psets': []})['psets']:
        pset = Data.psets[pset_id]
        if pset['Name'] == 'Pset_WallCommon' and 'LoadBearing' in [p['Name'] for p in pset['Properties']]:
            ifcopenshell.api.run("pset.edit_pset", f, pset=f.by_id(pset_id), properties={'LoadBearing': True}) # New version
            blenderbim.bim.module.pset.edit_pset.Usecase(f, {'pset': f.by_id(pset_id), 'properties': {'LoadBearing': True}}).execute() # Old version
    

    Benefits of doing it this way is:

    1. Less code
    2. Less mistakes - you don't need to handle the differences between getting psets of products, type products, and materials
    3. It also normalises access between ifc2x3 and ifc4 where necessary
    4. Running the usecases instead of setting manually also take into consideration pset template definitions and data casting
    5. It will also update ownership histories
    6. More readable! Especially when doing more complex operations.

    The API is pretty fresh so it needs time to mature, but I highly recommend checking it out. The more people using it, the more we can polish it into a really slick interface and iron out the bugs so that future devs can write less code with more features, faster.

    mathib
  • thanks, will check this out!

  • @Moult following is a test of this new ifcopenshell api. Let me know if I'm on the right track.

    I'm not using any blender specific code yet, I'm hoping that there is some magic to conjure up blender geometry from this ifc data (or maybe it is the other way around, I'm clueless!). IfcOwnerHistory doesn't seem to work for me, though I haven't really dug into this. I think I can work with numpy matrices for transforms, is this recommended? There doesn't seem to be an api for assembling geometry, so I'm constructing a SweptSolid the old fashioned way, is this ok?

    #!/usr/bin/python3
    import numpy
    import ifcopenshell.api
    
    # save some typing
    run = ifcopenshell.api.run
    
    ifc = run("project.create_file")
    
    # try and set up ownerhistory
    myperson = run("owner.add_person", ifc)
    myorganisation = run("owner.add_organisation", ifc)
    ownerhistory = run(
        "owner.create_owner_history",
        ifc,
        person=myperson,
        organisation=myorganisation,
    )  # doesn't seem to do anything
    
    # create a "project > site > building > storey" hierarchy
    project = run(
        "root.create_entity",
        ifc,
        ifc_class="IfcProject",
        name="My Project",
    )
    
    run("unit.assign_unit", ifc, length={"is_metric": True, "raw": "METERS"})
    
    mycontext = run("context.add_context", ifc)
    subcontext = run(
        "context.add_context",
        ifc,
        context="Model",
        subcontext="Body",
        target_view="MODEL_VIEW",
    )
    
    site = run("root.create_entity", ifc, ifc_class="IfcSite", name="My Site")
    building = run(
        "root.create_entity", ifc, ifc_class="IfcBuilding", name="My Building"
    )
    storey = run(
        "root.create_entity", ifc, ifc_class="IfcBuildingStorey", name="My Storey"
    )
    
    run("aggregate.assign_object", ifc, product=site, relating_object=project)
    run("aggregate.assign_object", ifc, product=building, relating_object=site)
    run("aggregate.assign_object", ifc, product=storey, relating_object=building)
    
    # create a sweptsolid:
    # start with a 2D profile in the XY plane
    profile = ifc.createIfcArbitraryClosedProfileDef(
        "AREA",
        None,
        ifc.createIfcPolyline(
            [
                ifc.createIfcCartesianPoint((1.0, 0.1)),
                ifc.createIfcCartesianPoint((4.0, 0.2)),
                ifc.createIfcCartesianPoint((4.0, 3.1)),
                ifc.createIfcCartesianPoint((1.0, 3.1)),
                ifc.createIfcCartesianPoint((1.0, 0.1)), # last point is same as first, oh my
            ]
        ),
    )
    # this is just a default coordinate system
    axis = ifc.createIfcAxis2Placement3D(
        ifc.createIfcCartesianPoint((0.0, 0.0, 0.0)), None, None
    )
    # Z positive up direction
    direction = ifc.createIfcDirection([0.0, 0.0, 1.0])
    # extrude the profile 0.5m
    solid = ifc.createIfcExtrudedAreaSolid(profile, axis, direction, 0.5)
    # solid needs to be put in a shape
    shape = ifc.createIfcShapeRepresentation(
        subcontext, "Body", "SweptSolid", [solid]
    )
    
    # how do we use this as a placement relative to the storey origin?
    shift = ifc.createIfcAxis2Placement3D(
        ifc.createIfcCartesianPoint((0.0, 0.0, 3.0)), None, None
    )
    localplacement = ifc.createIfcLocalPlacement(axis, shift)
    
    # create an ifcslab without a representation
    slab = run("root.create_entity", ifc, ifc_class="IfcSlab", name="My Slab")
    # assign the shape created earlier as the ifcslab representation
    run("geometry.assign_representation", ifc, product=slab, representation=shape)
    # place the ifcslab, but an identity matrix doesn't do anything
    run("geometry.edit_object_placement", ifc, product=slab, matrix=numpy.eye(4))
    # put the slab in the storey
    run("spatial.assign_container", ifc, product=slab, relating_structure=storey)
    
    ifc.write("test.ifc")
    
    John
  • @brunopostle correct, the geometry.add_representation usecase is currently tightly coupled to Blender. The process, I would imagine, is to define multiple "adaptors" of the add_representation usecase that follow a defined interface, but cater to Blender, FreeCAD, Trimesh, etc. This work has not yet been done.

    Adding a person and organisation is good, but you should in general never call the owner.create_owner_history usecase directly unless you're doing custom magic. Instead, it is simply used internally by the API for other usecases to call. Your responsibility, should you wish to use ownership histories (in IFC4, it is optional), is to overload or monkey-patch the functions in ifcopenshell.api.owner.settings. This way, you can determine yourself which owner is making which API call. Here's a simple case for you:

    ifcopenshell.api.owner.settings.get_person = lambda ifc : myperson
    ifcopenshell.api.owner.settings.get_organisation = lambda ifc : myperson
    ifcopenshell.api.owner.settings.get_application = lambda ifc : *your app*?
    

    This code looks unnecessary to me:

    # how do we use this as a placement relative to the storey origin?
    shift = ifc.createIfcAxis2Placement3D(
        ifc.createIfcCartesianPoint((0.0, 0.0, 3.0)), None, None
    )
    localplacement = ifc.createIfcLocalPlacement(axis, shift)
    

    I think the issue is that you are first editing the placement, and then assigning the spatial container. The assign container usecase currently isn't clever enough to recalculate the placement to be relative. This is a bug. I will fix it. If you first assigned the container, and then edited the placement, then the PlacementRelTo will be correct.

    Hope it helps.

    brunopostle
  • @Moult thanks, very useful. Although I'm targeting blenderbim, I can see how in the short term I can just write code that generates an IFC file, as in the example - or are there any considerations I need to make? Is generating an IFC file or a blenderbim model (or a freecad model) just a matter of calling a different function at the end? I'm currently writing a temporary IFC file and importing it with blenderbim, but this is slow

  • @brunopostle the IFC model is the BlenderBIM Add-on model :) There is no difference anymore. Not sure what different function you might be referring to.

  • @Moult said:
    @brunopostle the IFC model is the BlenderBIM Add-on model :) There is no difference anymore. Not sure what different function you might be referring to.

    That's my understanding too! It is only that if I run the above script in the blender console I don't get a nice model in blenderbim, or IFC structure in the outliner, so I'm assuming there is a function to update the GUI.

  • @brunopostle correct. The ifcopenshell.api as the namespace suggests is agnostic of Blender. It allows anybody to author IFCs with a high-level API. The BlenderBIM Add-on then adds a graphical interface on top of the ifcopenshell.api functions, so that you can easily view results and interact with it graphically. You can also call the BlenderBIM Add-on's operators via script, so if you wanted to see your results created step-by-step in the Blender interface, you could call those instead, for example bpy.ops.bim.create_project().

    Right now, there is no easy way to say "hey I've updated the underlying IFC, can you refresh what you have in the Blender scene?" via code as a general process. In the future, there will be, but I haven't built that yet. For specific changes, in particular non-geometric changes, you can indeed refresh the Blender UI by calling the load() function on the various Data classes. However, for geometric changes, it can be a bit more tricky. I can show you how if you're interested.

  • @Moult ok, it suits me to write everything outside of blender (as I can write tests, and restarting blender after every change is a real drag). For the time-being I'll continue using a temporary IFC file with ifc.write() and import_ifc.IfcImporter(), and I'll worry about doing it without a temporary file when I have this working.

  • @brunopostle agreed, it is desirable to do that. One of the main drivers to split the code outside Blender was to make sure it was testable and maintainable.

  • Hi @Moult ! related to this thread, I am trying to understand the new ifcopenshell.api after the refactor, but am struggling a little.

    If I try to set up a property set for a dummy IfcSlab/Cube, I do get the pset on BlenderBIM after updating the UI, but not its property from the second api.run (life of 100 years in this case):

    import bpy
    import ifcopenshell
    from ifcopenshell.api.pset.data import Data
    from blenderbim.bim.ifc import IfcStore
    
    file = IfcStore.get_file()
    obj = bpy.data.objects['IfcSlab/Cube']
    obj_id = obj.BIMObjectProperties.ifc_definition_id
    pset_to_add = ('Pset_ServiceLife', {'MeanTimeBetweenFailure': '100 years'})
    
    ifcopenshell.api.run(
        'pset.add_pset',
        file,
        product=file.by_id(obj_id),
        Name=pset_to_add[0]
    )
    
    for definition in file.by_id(obj_id).IsDefinedBy:
        if definition.is_a('IfcRelDefinesByProperties') and definition.RelatingPropertyDefinition.is_a('IfcPropertySet'):
            pset = definition.RelatingPropertyDefinition
            break
    
    for prop_name, prop_value in pset_to_add[1].items():
        ifcopenshell.api.run(
            'pset.edit_pset',
            file,
            pset=pset,
            properties={prop_name: prop_value}
        )
    

    Would this be more or less the preferred way to add property sets programatically or am I messing something up?

    Thanks in advance,

  • @cvillagrasa you've almost got it! Two mistakes:

    1. properties should be Properties with a capital P.
    2. 100 years is not a valid duration. Durations are a special data type in IFC. See https://en.wikipedia.org/wiki/ISO_8601#Durations - the correct value would be P100Y. Note that very soon it is likely we will build some utility functions for managing ISO8601 durations to make things like this simpler.
    cvillagrasa
  • Thanks a lot for the answer!
    #1 would have definitely entertained me for a while, and it's good to learn about #2.
    As a last step, I've also tried to update the UI with a tag_redraw, although I've desisted for now. I've tried as follows but it does not work:

    def refresh_object_properties():
        areas = bpy.data.screens['Layout'].areas
        idx_area = [idx for idx, area in enumerate(areas) if area.ui_type == 'PROPERTIES'][0]
        areas[idx_area].spaces.active.context = 'OBJECT'
        regions = bpy.data.screens['Layout'].areas[idx_area].regions
        idx_region = [idx for idx, region in enumerate(regions) if region.type == 'WINDOW'][0]
        bpy.data.screens['Layout'].areas[idx_area].regions[idx_region].tag_redraw()
    

    Once in the window region of the properties area, I don't really know how to navigate down to the actual BIM_PT_object_psets panel, and the general tag_redraw seems to have no effect. But in any case, this is a very minor issue for now. Adding (to then delete) an additional pset does trigger the update.

  • edited April 2021

    @cvillagrasa the UI basically relies on IFC data cached in the relevant API data class. There is no need to navigate to any Blender UI region and do any fancy redraw function. Try this:

    from ifcopenshell.api.pset.data import Data
    Data.load(file, file.by_id(obj_id))
    
    cvillagrasa
  • When using

    from blenderbim.bim.ifc import IfcStore
    f = IfcStore.get_file()
    

    How do I access the absolute file path of the IFC file?

  • @Coen said:
    When using

    from blenderbim.bim.ifc import IfcStore
    f = IfcStore.get_file()
    

    How do I access the absolute file path of the IFC file?

    Nevermind, found it. It's

    IfcStore.path
    
Sign In or Register to comment.