# *************************************************************************** # * * # * Copyright (c) 2022 Yorik van Havre * # * * # * This program is free software; you can redistribute it and/or modify * # * it under the terms of the GNU Lesser General Public License (LGPL) * # * as published by the Free Software Foundation; either version 2 of * # * the License, or (at your option) any later version. * # * for detail see the LICENCE text file. * # * * # * This program is distributed in the hope that it will be useful, * # * but WITHOUT ANY WARRANTY; without even the implied warranty of * # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * # * GNU Library General Public License for more details. * # * * # * You should have received a copy of the GNU Library General Public * # * License along with this program; if not, write to the Free Software * # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * # * USA * # * * # *************************************************************************** """This is the main NativeIFC module""" import os # heavyweight libraries - ifc_tools should always be lazy loaded import FreeCAD import Draft import Arch from importers import exportIFC from importers import exportIFCHelper import ifcopenshell from ifcopenshell import geom from ifcopenshell import api from ifcopenshell import template from ifcopenshell.util import element from ifcopenshell.util import attribute from ifcopenshell.util import schema from ifcopenshell.util import placement from ifcopenshell.util import unit from nativeifc import ifc_objects from nativeifc import ifc_viewproviders from nativeifc import ifc_import from nativeifc import ifc_layers from nativeifc import ifc_status SCALE = 1000.0 # IfcOpenShell works in meters, FreeCAD works in mm SHORT = False # If True, only Step ID attribute is created ROUND = 8 # rounding value for placements DEFAULT_SHAPEMODE = "Coin" # Can be Shape, Coin or None PARAMS = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/NativeIFC") def create_document(document, filename=None, shapemode=0, strategy=0, silent=False): """Creates a IFC document object in the given FreeCAD document or converts that document into an IFC document, depending on the state of the statusbar lock button. filename: If not given, a blank IFC document is created shapemode: 0 = full shape 1 = coin only 2 = no representation strategy: 0 = only root object 1 = only bbuilding structure, 2 = all children """ if ifc_status.get_lock_status(): return convert_document(document, filename, shapemode, strategy, silent) else: return create_document_object(document, filename, shapemode, strategy, silent) def create_document_object( document, filename=None, shapemode=0, strategy=0, silent=False ): """Creates a IFC document object in the given FreeCAD document. filename: If not given, a blank IFC document is created shapemode: 0 = full shape 1 = coin only 2 = no representation strategy: 0 = only root object 1 = only bbuilding structure, 2 = all children """ obj = add_object(document, otype="project") ifcfile, project, full = setup_project(obj, filename, shapemode, silent) # populate according to strategy if strategy == 0: pass elif strategy == 1: create_children(obj, ifcfile, recursive=True, only_structure=True) elif strategy == 2: create_children(obj, ifcfile, recursive=True, assemblies=False) # create default structure if full: site = aggregate(Arch.makeSite(), obj) building = aggregate(Arch.makeBuilding(), site) storey = aggregate(Arch.makeFloor(), building) return obj def convert_document(document, filename=None, shapemode=0, strategy=0, silent=False): """Converts the given FreeCAD document to an IFC document. filename: If not given, a blank IFC document is created shapemode: 0 = full shape 1 = coin only 2 = no representation strategy: 0 = only root object 1 = only bbuilding structure 2 = all children 3 = no children """ if not "Proxy" in document.PropertiesList: document.addProperty("App::PropertyPythonObject", "Proxy") document.setPropertyStatus("Proxy", "Transient") document.Proxy = ifc_objects.document_object() ifcfile, project, full = setup_project(document, filename, shapemode, silent) if strategy == 0: create_children(document, ifcfile, recursive=False) elif strategy == 1: create_children(document, ifcfile, recursive=True, only_structure=True) elif strategy == 2: create_children(document, ifcfile, recursive=True, assemblies=False) elif strategy == 3: pass # create default structure if full: site = aggregate(Arch.makeSite(), document) building = aggregate(Arch.makeBuilding(), site) storey = aggregate(Arch.makeFloor(), building) return document def setup_project(proj, filename, shapemode, silent): """Sets up a project (common operations between single doc/not single doc modes) Returns the ifcfile object, the project ifc entity, and full (True/False)""" full = False d = "The path to the linked IFC file" if not "IfcFilePath" in proj.PropertiesList: proj.addProperty("App::PropertyFile", "IfcFilePath", "Base", d) if not "Modified" in proj.PropertiesList: proj.addProperty("App::PropertyBool", "Modified", "Base") proj.setPropertyStatus("Modified", "Hidden") if filename: # opening existing file proj.IfcFilePath = filename ifcfile = ifcopenshell.open(filename) else: # creating a new file if not silent: full = ifc_import.get_project_type() ifcfile = create_ifcfile() project = ifcfile.by_type("IfcProject")[0] # TODO configure version history # https://blenderbim.org/docs-python/autoapi/ifcopenshell/api/owner/create_owner_history/index.html # In IFC4, history is optional. What should we do here? proj.Proxy.ifcfile = ifcfile add_properties(proj, ifcfile, project, shapemode=shapemode) if not "Schema" in proj.PropertiesList: proj.addProperty("App::PropertyEnumeration", "Schema", "Base") # bug in FreeCAD - to avoid a crash, pre-populate the enum with one value proj.Schema = [ifcfile.wrapped_data.schema_name()] proj.Schema = ifcfile.wrapped_data.schema_name() proj.Schema = ifcopenshell.ifcopenshell_wrapper.schema_names() return ifcfile, project, full def create_ifcfile(): """Creates a new, empty IFC document""" ifcfile = api_run("project.create_file") project = api_run("root.create_entity", ifcfile, ifc_class="IfcProject") param = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Document") user = param.GetString("prefAuthor", "") user = user.split("<")[0].strip() if user: person = api_run("owner.add_person", ifcfile, family_name=user) org = param.GetString("prefCompany", "") if org: organisation = api_run("owner.add_organisation", ifcfile, name=org) if user and org: api_run( "owner.add_person_and_organisation", ifcfile, person=person, organisation=organisation, ) application = "FreeCAD" version = FreeCAD.Version() version = ".".join([str(v) for v in version[0:3]]) application = api_run( "owner.add_application", ifcfile, application_full_name=application, version=version, ) # context model3d = api_run("context.add_context", ifcfile, context_type="Model") plan = api_run("context.add_context", ifcfile, context_type="Plan") body = api_run( "context.add_context", ifcfile, context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model3d, ) api_run( "context.add_context", ifcfile, context_type="Model", context_identifier="Axis", target_view="GRAPH_VIEW", parent=model3d, ) # unit # for now, assign a default metre unit, as per https://blenderbim.org/docs-python/autoapi/ifcopenshell/api/unit/assign_unit/index.html # TODO allow to set this at creation, from the current FreeCAD units schema api_run("unit.assign_unit", ifcfile) return ifcfile def api_run(*args, **kwargs): """Runs an IfcOpenShell API call and flags the ifcfile as modified""" result = ifcopenshell.api.run(*args, **kwargs) # *args are typically command, ifcfile if len(args) > 1: ifcfile = args[1] for d in FreeCAD.listDocuments().values(): for o in d.Objects: if hasattr(o, "Proxy") and hasattr(o.Proxy, "ifcfile"): if o.Proxy.ifcfile == ifcfile: o.Modified = True return result def create_object(ifcentity, document, ifcfile, shapemode=0): """Creates a FreeCAD object from an IFC entity""" exobj = get_object(ifcentity, document) if exobj: return exobj s = "IFC: Created #{}: {}, '{}'\n".format( ifcentity.id(), ifcentity.is_a(), ifcentity.Name ) FreeCAD.Console.PrintLog(s) obj = add_object(document) add_properties(obj, ifcfile, ifcentity, shapemode=shapemode) ifc_layers.add_layers(obj, ifcentity, ifcfile) if FreeCAD.GuiUp: if ifcentity.is_a("IfcSpace") or ifcentity.is_a("IfcOpeningElement"): obj.ViewObject.DisplayMode = "Wireframe" elements = [ifcentity] return obj def create_children( obj, ifcfile=None, recursive=False, only_structure=False, assemblies=True, expand=False, ): """Creates a hierarchy of objects under an object""" def get_parent_objects(parent): proj = get_project(parent) if hasattr(proj, "OutListRecursive"): return proj.OutListRecursive elif hasattr(proj, "Objects"): return proj.Objects def create_child(parent, element): subresult = [] # do not create if a child with same stepid already exists if not element.id() in [ getattr(c, "StepId", 0) for c in get_parent_objects(parent) ]: doc = getattr(parent, "Document", parent) mode = getattr(parent, "ShapeMode", "Coin") child = create_object(element, doc, ifcfile, mode) subresult.append(child) if isinstance(parent, FreeCAD.DocumentObject): parent.Proxy.addObject(parent, child) if element.is_a("IfcSite"): # force-create contained buildings too if we just created a site buildings = [ o for o in get_children(child, ifcfile) if o.is_a("IfcBuilding") ] for building in buildings: subresult.extend(create_child(child, building)) elif element.is_a("IfcOpeningElement"): # force-create contained windows too if we just created an opening windows = [ o for o in get_children(child, ifcfile) if o.is_a() in ("IfcWindow", "IfcDoor") ] for window in windows: subresult.extend(create_child(child, window)) if recursive: subresult.extend( create_children( child, ifcfile, recursive, only_structure, assemblies ) ) return subresult if not ifcfile: ifcfile = get_ifcfile(obj) result = [] children = get_children(obj, ifcfile, only_structure, assemblies, expand) for child in children: result.extend(create_child(obj, child)) assign_groups(children) return result def assign_groups(children): """Fill the groups inthis list""" for child in children: if child.is_a("IfcGroup"): mode = "IsGroupedBy" elif child.is_a("IfcElementAssembly"): mode = "IsDecomposedBy" else: mode = None if mode: grobj = get_object(child) for rel in getattr(child, mode): for elem in rel.RelatedObjects: elobj = get_object(elem) if elobj: if len(elobj.InList) == 1: p = elobj.InList[0] if elobj in p.Group: g = p.Group g.remove(elobj) p.Group = g g = grobj.Group g.append(elobj) grobj.Group = g def get_children( obj, ifcfile=None, only_structure=False, assemblies=True, expand=False, iftype=None ): """Returns the direct descendants of an object""" if not ifcfile: ifcfile = get_ifcfile(obj) ifcentity = ifcfile[obj.StepId] children = [] if assemblies or not ifcentity.is_a("IfcElement"): for rel in getattr(ifcentity, "IsDecomposedBy", []): children.extend(rel.RelatedObjects) if not only_structure: for rel in getattr(ifcentity, "ContainsElements", []): children.extend(rel.RelatedElements) for rel in getattr(ifcentity, "HasOpenings", []): children.extend([rel.RelatedOpeningElement]) for rel in getattr(ifcentity, "HasFillings", []): children.extend([rel.RelatedBuildingElement]) result = filter_elements( children, ifcfile, expand=expand, spaces=True, assemblies=assemblies ) if iftype: result = [r for r in result if r.is_a(ifctype)] return result def get_object(element, document=None): """Returns the object that references this element, if any""" if document: ldocs = {"document": document} else: ldocs = FreeCAD.listDocuments() for n, d in ldocs.items(): for obj in d.Objects: if hasattr(obj, "StepId"): if obj.StepId == element.id(): if get_ifc_element(obj) == element: return obj return None def get_ifcfile(obj): """Returns the ifcfile that handles this object""" project = get_project(obj) if project: if getattr(project, "Proxy", None): if hasattr(project.Proxy, "ifcfile"): return project.Proxy.ifcfile if project.IfcFilePath: ifcfile = ifcopenshell.open(project.IfcFilePath) if hasattr(project, "Proxy"): if project.Proxy is None: if not isinstance(project, FreeCAD.DocumentObject): project.Proxy = ifc_objects.document_object() if getattr(project, "Proxy", None): project.Proxy.ifcfile = ifcfile return ifcfile return None def get_project(obj): """Returns the ifc document this object belongs to. obj can be either a document object, an ifcfile or ifc element instance""" proj_types = ("IfcProject", "IfcProjectLibrary") if isinstance(obj, ifcopenshell.file): for d in FreeCAD.listDocuments().values(): for o in d.Objects: if hasattr(o, "Proxy") and hasattr(o.Proxy, "ifcfile"): if o.Proxy.ifcfile == obj: return o return None if isinstance(obj, ifcopenshell.entity_instance): obj = get_object(obj) if hasattr(obj, "IfcFilePath"): return obj if hasattr(getattr(obj, "Document", None), "IfcFilePath"): return obj.Document if getattr(obj, "Class", None) in proj_types: return obj if hasattr(obj, "InListRecursive"): for parent in obj.InListRecursive: if getattr(parent, "Class", None) in proj_types: return parent return None def can_expand(obj, ifcfile=None): """Returns True if this object can have any more child extracted""" if not ifcfile: ifcfile = get_ifcfile(obj) children = get_children(obj, ifcfile, expand=True) group = [o.StepId for o in obj.Group if hasattr(o, "StepId")] for child in children: if child.id() not in group: return True return False def add_object(document, otype=None, oname="IfcObject"): """adds a new object to a FreeCAD document. otype can be 'project', 'group', 'material', 'layer' or None (normal object)""" if not document: return None proxy = ifc_objects.ifc_object(otype) if otype == "group": proxy = None ftype = "App::DocumentObjectGroupPython" elif otype == "material": ftype = "App::MaterialObjectPython" elif otype == "layer": ftype = "App::FeaturePython" else: ftype = "Part::FeaturePython" if otype == "project": vp = ifc_viewproviders.ifc_vp_document() elif otype == "group": vp = ifc_viewproviders.ifc_vp_group() elif otype == "material": vp = ifc_viewproviders.ifc_vp_material() elif otype == "layer": vp = None else: vp = ifc_viewproviders.ifc_vp_object() obj = document.addObject(ftype, oname, proxy, vp, False) if obj.ViewObject and otype == "layer": from draftviewproviders import view_layer # lazy import view_layer.ViewProviderLayer(obj.ViewObject) obj.ViewObject.addProperty("App::PropertyBool", "HideChildren", "Layer") obj.ViewObject.HideChildren = True return obj def add_properties( obj, ifcfile=None, ifcentity=None, links=False, shapemode=0, short=SHORT ): """Adds the properties of the given IFC object to a FreeCAD object""" if not ifcfile: ifcfile = get_ifcfile(obj) if not ifcentity: ifcentity = get_ifc_element(obj) if getattr(ifcentity, "Name", None): obj.Label = ifcentity.Name elif getattr(obj, "IfcFilePath", ""): obj.Label = os.path.splitext(os.path.basename(obj.IfcFilePath))[0] else: obj.Label = "_" + ifcentity.is_a() if isinstance(obj, FreeCAD.DocumentObject) and "Group" not in obj.PropertiesList: obj.addProperty("App::PropertyLinkList", "Group", "Base") if "ShapeMode" not in obj.PropertiesList: obj.addProperty("App::PropertyEnumeration", "ShapeMode", "Base") shapemodes = [ "Shape", "Coin", "None", ] # possible shape modes for all IFC objects if isinstance(shapemode, int): shapemode = shapemodes[shapemode] obj.ShapeMode = shapemodes obj.ShapeMode = shapemode if not obj.isDerivedFrom("Part::Feature"): obj.setPropertyStatus("ShapeMode", "Hidden") attr_defs = ifcentity.wrapped_data.declaration().as_entity().all_attributes() try: info_ifcentity = ifcentity.get_info() except: # slower but no errors info_ifcentity = get_elem_attribs(ifcentity) for attr, value in info_ifcentity.items(): if attr == "type": attr = "Class" elif attr == "id": attr = "StepId" elif attr == "Name": continue if short and attr not in ("Class", "StepId"): continue attr_def = next((a for a in attr_defs if a.name() == attr), None) data_type = ( ifcopenshell.util.attribute.get_primitive_type(attr_def) if attr_def else None ) if attr == "Class": # main enum property, not saved to file if attr not in obj.PropertiesList: obj.addProperty("App::PropertyEnumeration", attr, "IFC") obj.setPropertyStatus(attr, "Transient") # to avoid bug/crash: we populate first the property with only the # class, then we add the sibling classes setattr(obj, attr, [value]) setattr(obj, attr, value) setattr(obj, attr, get_ifc_classes(obj, value)) # companion hidden propertym that gets saved to file if "IfcClass" not in obj.PropertiesList: obj.addProperty("App::PropertyString", "IfcClass", "IFC") obj.setPropertyStatus("IfcClass", "Hidden") setattr(obj, "IfcClass", value) elif attr_def and "IfcLengthMeasure" in str(attr_def.type_of_attribute()): obj.addProperty("App::PropertyDistance", attr, "IFC") if value: setattr(obj, attr, value * (1 / get_scale(ifcfile))) elif isinstance(value, int): if attr not in obj.PropertiesList: obj.addProperty("App::PropertyInteger", attr, "IFC") if attr == "StepId": obj.setPropertyStatus(attr, "ReadOnly") setattr(obj, attr, value) elif isinstance(value, float): if attr not in obj.PropertiesList: obj.addProperty("App::PropertyFloat", attr, "IFC") setattr(obj, attr, value) elif data_type == "boolean": if attr not in obj.PropertiesList: obj.addProperty("App::PropertyBool", attr, "IFC") if not value or value in ["UNKNOWN", "FALSE"]: value = False elif not isinstance(value, bool): print("DEBUG: attempting to set boolean value:", attr, value) value = bool(value) setattr(obj, attr, value) # will trigger error. TODO: Fix this elif isinstance(value, ifcopenshell.entity_instance): if links: if attr not in obj.PropertiesList: # value = create_object(value, obj.Document) obj.addProperty("App::PropertyLink", attr, "IFC") # setattr(obj, attr, value) elif isinstance(value, (list, tuple)) and value: if isinstance(value[0], ifcopenshell.entity_instance): if links: if attr not in obj.PropertiesList: # nvalue = [] # for elt in value: # nvalue.append(create_object(elt, obj.Document)) obj.addProperty("App::PropertyLinkList", attr, "IFC") # setattr(obj, attr, nvalue) elif data_type == "enum": if attr not in obj.PropertiesList: obj.addProperty("App::PropertyEnumeration", attr, "IFC") items = ifcopenshell.util.attribute.get_enum_items(attr_def) if value not in items: for v in ("UNDEFINED", "NOTDEFINED", "USERDEFINED"): if v in items: value = v break if value in items: # to prevent bug/crash, we first need to populate the # enum with the value about to be used, then # add the alternatives setattr(obj, attr, [value]) setattr(obj, attr, value) setattr(obj, attr, items) else: if attr not in obj.PropertiesList: obj.addProperty("App::PropertyString", attr, "IFC") if value is not None: setattr(obj, attr, str(value)) # link Label2 and Description if "Description" in obj.PropertiesList and hasattr(obj, "setExpression"): obj.setExpression("Label2", "Description") def remove_unused_properties(obj): """Remove IFC properties if they are not part of the current IFC class""" elt = get_ifc_element(obj) props = list(elt.get_info().keys()) props[props.index("id")] = "StepId" props[props.index("type")] = "Class" for prop in obj.PropertiesList: if obj.getGroupOfProperty(prop) == "IFC": if prop not in props: obj.removeProperty(prop) def get_ifc_classes(obj, baseclass): """Returns a list of sibling classes from a given FreeCAD object""" # this function can become pure IFC if baseclass in ("IfcProject", "IfcProjectLibrary"): return ("IfcProject", "IfcProjectLibrary") ifcfile = get_ifcfile(obj) if not ifcfile: return [baseclass] classes = [] schema = ifcfile.wrapped_data.schema_name() schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema) declaration = schema.declaration_by_name(baseclass) if "StandardCase" in baseclass: declaration = declaration.supertype() if declaration.supertype(): # include sibling classes classes = [sub.name() for sub in declaration.supertype().subtypes()] # include superclass too so one can "navigate up" classes.append(declaration.supertype().name()) # also include subtypes of the current class (ex, StandardCases) classes.extend([sub.name() for sub in declaration.subtypes()]) if baseclass not in classes: classes.append(baseclass) return classes def get_ifc_element(obj, ifcfile=None): """Returns the corresponding IFC element of an object""" if not ifcfile: ifcfile = get_ifcfile(obj) if ifcfile and hasattr(obj, "StepId"): try: return ifcfile.by_id(obj.StepId) except RuntimeError: # entity not found pass return None def has_representation(element): """Tells if an elements has an own representation""" # This function can become pure IFC if hasattr(element, "Representation") and element.Representation: return True return False def filter_elements(elements, ifcfile, expand=True, spaces=False, assemblies=True): """Filter elements list of unwanted classes""" # This function can become pure IFC # gather decomposition if needed if not isinstance(elements, (list, tuple)): elements = [elements] openings = False if assemblies and any([e.is_a("IfcOpeningElement") for e in elements]): openings = True if expand and (len(elements) == 1): elem = elements[0] if elem.is_a("IfcSpace"): spaces = True if not has_representation(elem): if elem.is_a("IfcProject"): elements = ifcfile.by_type("IfcElement") elements.extend(ifcfile.by_type("IfcSite")) else: decomp = ifcopenshell.util.element.get_decomposition(elem) if decomp: # avoid replacing elements if decomp is empty elements = decomp else: if elem.Representation.Representations: rep = elem.Representation.Representations[0] if ( rep.Items and rep.Items[0].is_a() == "IfcPolyline" and elem.IsDecomposedBy ): # only use the decomposition and not the polyline # happens for multilayered walls exported by VectorWorks # the Polyline is the wall axis # see https://github.com/yorikvanhavre/FreeCAD-NativeIFC/issues/28 elements = ifcopenshell.util.element.get_decomposition(elem) if not openings: # Never load feature elements by default, they can be lazy loaded elements = [e for e in elements if not e.is_a("IfcFeatureElement")] # do load spaces when required, otherwise skip computing their shapes if not spaces: elements = [e for e in elements if not e.is_a("IfcSpace")] # skip projects elements = [e for e in elements if not e.is_a("IfcProject")] # skip furniture for now, they can be lazy loaded probably elements = [e for e in elements if not e.is_a("IfcFurnishingElement")] # skip annotations for now elements = [e for e in elements if not e.is_a("IfcAnnotation")] return elements def set_attribute(ifcfile, element, attribute, value): """Sets the value of an attribute of an IFC element""" # This function can become pure IFC if not ifcfile or not element: return False if isinstance(value, FreeCAD.Units.Quantity): f = get_scale(ifcfile) value = value.Value * f if attribute == "Class": if value != element.is_a(): if value and value.startswith("Ifc"): cmd = "root.reassign_class" FreeCAD.Console.PrintLog( "Changing IFC class value: " + element.is_a() + " to " + str(value) + "\n" ) product = api_run(cmd, ifcfile, product=element, ifc_class=value) # TODO fix attributes return product cmd = "attribute.edit_attributes" attribs = {attribute: value} if hasattr(element, attribute): if ( attribute == "Name" and getattr(element, attribute) is None and value.startswith("_") ): # do not consider default FreeCAD names given to unnamed alements return False if getattr(element, attribute) != value: FreeCAD.Console.PrintLog( "Changing IFC attribute value of " + str(attribute) + ": " + str(value) + "\n" ) api_run(cmd, ifcfile, product=element, attributes=attribs) return True return False def set_colors(obj, colors): """Sets the given colors to an object""" if FreeCAD.GuiUp and colors: try: vobj = obj.ViewObject except ReferenceError: # Object was probably deleted return # ifcopenshell issues (-1,-1,-1) colors if not set if isinstance(colors[0], (tuple, list)): colors = [tuple([abs(d) for d in c]) for c in colors] else: colors = [abs(c) for c in colors] if hasattr(vobj, "ShapeColor"): if isinstance(colors[0], (tuple, list)): vobj.ShapeColor = colors[0][:3] # do not set transparency when the object has more than one color #if len(colors[0]) > 3: # vobj.Transparency = int(colors[0][3] * 100) else: vobj.ShapeColor = colors[:3] if len(colors) > 3: vobj.Transparency = int(colors[3] * 100) if hasattr(vobj, "DiffuseColor"): # strip out transparency value because it currently gives ugly # results in FreeCAD when combining transparent and non-transparent objects if all([len(c) > 3 and c[3] != 0 for c in colors]): vobj.DiffuseColor = colors else: vobj.DiffuseColor = [c[:3] for c in colors] def get_body_context_ids(ifcfile): # This function can become pure IFC # Facetation is to accommodate broken Revit files # See https://forums.buildingsmart.org/t/suggestions-on-how-to-improve-clarity\ # -of-representation-context-usage-in-documentation/3663/6?u=moult body_contexts = [ c.id() for c in ifcfile.by_type("IfcGeometricRepresentationSubContext") if c.ContextIdentifier in ["Body", "Facetation"] ] # Ideally, all representations should be in a subcontext, but some BIM apps don't do this # correctly, so we add main contexts too body_contexts.extend( [ c.id() for c in ifcfile.by_type( "IfcGeometricRepresentationContext", include_subtypes=False ) if c.ContextType == "Model" ] ) return body_contexts def get_plan_contexts_ids(ifcfile): # This function can become pure IFC # Annotation is to accommodate broken Revit files # See https://github.com/Autodesk/revit-ifc/issues/187 return [ c.id() for c in ifcfile.by_type("IfcGeometricRepresentationContext") if c.ContextType in ["Plan", "Annotation"] ] def get_freecad_matrix(ios_matrix): """Converts an IfcOpenShell matrix tuple into a FreeCAD matrix""" # https://github.com/IfcOpenShell/IfcOpenShell/issues/1440 # https://pythoncvc.net/?cat=203 # https://github.com/IfcOpenShell/IfcOpenShell/issues/4832#issuecomment-2158583873 m_l = list() for i in range(3): if len(ios_matrix) == 16: # IfcOpenShell 0.8 line = list(ios_matrix[i::4]) else: # IfcOpenShell 0.7 line = list(ios_matrix[i::3]) line[-1] *= SCALE m_l.extend(line) return FreeCAD.Matrix(*m_l) def get_ios_matrix(m): """Converts a FreeCAD placement or matrix into an IfcOpenShell matrix tuple""" if isinstance(m, FreeCAD.Placement): m = m.Matrix mat = [ [m.A11, m.A12, m.A13, m.A14], [m.A21, m.A22, m.A23, m.A24], [m.A31, m.A32, m.A33, m.A34], [m.A41, m.A42, m.A42, m.A44], ] # apply rounding because OCCT often changes 1.0 to 0.99999999999 or something rmat = [] for row in mat: rmat.append([round(e, ROUND) for e in row]) return rmat def get_scale(ifcfile): """Returns the scale factor to convert any file length to mm""" scale = ifcopenshell.util.unit.calculate_unit_scale(ifcfile) # the above lines yields meter -> file unit scale factor. We need mm return 0.001 / scale def set_placement(obj): """Updates the internal IFC placement according to the object placement""" # This function can become pure IFC ifcfile = get_ifcfile(obj) if not ifcfile: print("DEBUG: No ifc file for object", obj.Label, "Aborting") if obj.Class in ["IfcProject", "IfcProjectLibrary"]: return element = get_ifc_element(obj) placement = FreeCAD.Placement(obj.Placement) placement.Base = FreeCAD.Vector(placement.Base).multiply(get_scale(ifcfile)) new_matrix = get_ios_matrix(placement) old_matrix = ifcopenshell.util.placement.get_local_placement( element.ObjectPlacement ) # conversion from numpy array old_matrix = old_matrix.tolist() old_matrix = [[round(c, ROUND) for c in r] for r in old_matrix] if new_matrix != old_matrix: FreeCAD.Console.PrintLog( "IFC: placement changed for " + obj.Label + " old: " + str(old_matrix) + " new: " + str(new_matrix) + "\n" ) api = "geometry.edit_object_placement" api_run(api, ifcfile, product=element, matrix=new_matrix, is_si=False) return True return False def save_ifc(obj, filepath=None): """Saves the linked IFC file of a project, but does not mark it as saved""" if not filepath: if getattr(obj, "IfcFilePath", None): filepath = obj.IfcFilePath if filepath: ifcfile = get_ifcfile(obj) if not ifcfile: ifcfile = create_ifcfile() ifcfile.write(filepath) FreeCAD.Console.PrintMessage("Saved " + filepath + "\n") def save(obj, filepath=None): """Saves the linked IFC file of a project and set its saved status""" save_ifc(obj, filepath) obj.Modified = False def aggregate(obj, parent): """Takes any FreeCAD object and aggregates it to an existing IFC object""" proj = get_project(parent) if not proj: FreeCAD.Console.PrintError("The parent object is not part of an IFC project\n") return ifcfile = get_ifcfile(proj) product = None stepid = getattr(obj, "StepId", None) if stepid: # obj might be dragging at this point and has no project anymore try: elem = ifcfile[stepid] if obj.GlobalId == elem.GlobalId: product = elem except: pass if product: # this object already has an associated IFC product print("DEBUG:", obj.Label, "is already part of the IFC document") newobj = obj new = False else: product = create_product(obj, parent, ifcfile) shapemode = getattr(parent, "ShapeMode", DEFAULT_SHAPEMODE) newobj = create_object(product, obj.Document, ifcfile, shapemode) new = True create_relationship(obj, newobj, parent, product, ifcfile) base = getattr(obj, "Base", None) if base: # make sure the base is used only by this object before deleting if base.InList != [obj]: base = None # handle layer if FreeCAD.GuiUp: import FreeCADGui autogroup = getattr( getattr(FreeCADGui, "draftToolBar", None), "autogroup", None ) if autogroup is not None: layer = FreeCAD.ActiveDocument.getObject(autogroup) if hasattr(layer, "StepId"): ifc_layers.add_to_layer(newobj, layer) # aggregate dependent objects for child in obj.InList: if hasattr(child,"Host") and child.Host == obj: aggregate(child, newobj) elif hasattr(child,"Hosts") and obj in child.Hosts: #op = create_product(child, newobj, ifcfile, ifcclass="IfcOpeningElement") aggregate(child, newobj) delete = not (PARAMS.GetBool("KeepAggregated", False)) if new and delete and base: obj.Document.removeObject(base.Name) label = obj.Label if new and delete: obj.Document.removeObject(obj.Name) if new: newobj.Label = label # to avoid 001-ing the Label... return newobj def deaggregate(obj, parent): """Removes a FreeCAD object form its parent""" ifcfile = get_ifcfile(obj) element = get_ifc_element(obj) if not element: return try: api_run("aggregate.unassign_object", ifcfile, products=[element]) except: # older version of ifcopenshell api_run("aggregate.unassign_object", ifcfile, product=element) parent.Proxy.removeObject(parent, obj) def create_product(obj, parent, ifcfile, ifcclass=None): """Creates an IFC product out of a FreeCAD object""" name = obj.Label description = getattr(obj, "Description", None) if not ifcclass: ifcclass = get_ifctype(obj) representation, placement = create_representation(obj, ifcfile) product = api_run("root.create_entity", ifcfile, ifc_class=ifcclass, name=name) set_attribute(ifcfile, product, "Description", description) set_attribute(ifcfile, product, "ObjectPlacement", placement) # TODO below cannot be used at the moment because the ArchIFC exporter returns an # IfcProductDefinitionShape already and not an IfcShapeRepresentation # api_run("geometry.assign_representation", ifcfile, product=product, representation=representation) set_attribute(ifcfile, product, "Representation", representation) # TODO treat subtractions/additions return product def create_representation(obj, ifcfile): """Creates a geometry representation for the given object""" # TEMPORARY use the Arch exporter # TODO this is temporary. We should rely on ifcopenshell for this with: # https://blenderbim.org/docs-python/autoapi/ifcopenshell/api/root/create_entity/index.html # a new FreeCAD 'engine' should be added to: # https://blenderbim.org/docs-python/autoapi/ifcopenshell/api/geometry/index.html # that should contain all typical use cases one could have to convert FreeCAD geometry # to IFC. # setup exporter - TODO do that in the module init exportIFC.clones = {} exportIFC.profiledefs = {} exportIFC.surfstyles = {} exportIFC.shapedefs = {} exportIFC.ifcopenshell = ifcopenshell exportIFC.ifcbin = exportIFCHelper.recycler(ifcfile, template=False) prefs, context = get_export_preferences(ifcfile) representation, placement, shapetype = exportIFC.getRepresentation( ifcfile, context, obj, preferences=prefs ) return representation, placement def get_ifctype(obj): """Returns a valid IFC type from an object""" if hasattr(obj, "Class"): if "ifc" in str(obj.Class).lower(): return obj.Class if hasattr(obj,"IfcType") and obj.IfcType != "Undefined": return "Ifc" + obj.IfcType.replace(" ","") dtype = Draft.getType(obj) if dtype in ["App::Part","Part::Compound","Array"]: return "IfcElementAssembly" if dtype in ["App::DocumentObjectGroup"]: ifctype = "IfcGroup" return "IfcBuildingElementProxy" def get_export_preferences(ifcfile): """returns a preferences dict for exportIFC""" prefs = exportIFC.getPreferences() prefs["SCHEMA"] = ifcfile.wrapped_data.schema_name() s = ifcopenshell.util.unit.calculate_unit_scale(ifcfile) # the above lines yields meter -> file unit scale factor. We need mm prefs["SCALE_FACTOR"] = 0.001 / s context = ifcfile[ get_body_context_ids(ifcfile)[0] ] # we take the first one (first found subcontext) return prefs, context def get_subvolume(obj): """returns a subface + subvolume from a window object""" tempface = None tempobj = None tempshape = None if hasattr(obj, "Proxy") and hasattr(obj.Proxy, "getSubVolume"): tempshape = obj.Proxy.getSubVolume(obj) elif hasattr(obj, "Subvolume") and obj.Subvolume: tempshape = obj.Subvolume if tempshape: if len(tempshape.Faces) == 6: # We assume the standard output of ArchWindows faces = sorted(tempshape.Faces, key=lambda f: f.CenterOfMass.z) baseface = faces[0] ext = faces[-1].CenterOfMass.sub(faces[0].CenterOfMass) tempface = obj.Document.addObject("Part::Feature", "BaseFace") tempface.Shape = baseface tempobj = obj.Document.addObject("Part::Extrusion", "Opening") tempobj.Base = tempface tempobj.DirMode = "Custom" tempobj.Dir = FreeCAD.Vector(ext).normalize() tempobj.LengthFwd = ext.Length else: tempobj = obj.Document.addObject("Part::Feature", "Opening") tempobj.Shape = tempshape if tempobj: tempobj.recompute() return tempface, tempobj def create_relationship(old_obj, obj, parent, element, ifcfile): """Creates a relationship between an IFC object and a parent IFC object""" parent_element = get_ifc_element(parent) # case 1: element inside spatiual structure if parent_element.is_a("IfcSpatialStructureElement") and element.is_a("IfcElement"): # first remove the FreeCAD object from any parent for old_par in old_obj.InList: if hasattr(old_par, "Group") and old_obj in old_par.Group: old_par.Group = [o for o in old_par.Group if o != old_obj] try: api_run("spatial.unassign_container", ifcfile, products=[element]) except: # older version of IfcOpenShell api_run("spatial.unassign_container", ifcfile, product=element) if element.is_a("IfcOpeningElement"): uprel = api_run( "void.add_opening", ifcfile, opening=element, element=parent_element, ) else: try: uprel = api_run( "spatial.assign_container", ifcfile, products=[element], relating_structure=parent_element, ) except: # older version of ifcopenshell uprel = api_run( "spatial.assign_container", ifcfile, product=element, relating_structure=parent_element, ) # case 2: dooe/window inside element # https://standards.buildingsmart.org/IFC/RELEASE/IFC4/ADD2_TC1/HTML/annex/annex-e/wall-with-opening-and-window.htm elif parent_element.is_a("IfcElement") and element.is_a() in [ "IfcDoor", "IfcWindow", ]: tempface, tempobj = get_subvolume(old_obj) if tempobj: opening = create_product(tempobj, parent, ifcfile, "IfcOpeningElement") old_obj.Document.removeObject(tempobj.Name) if tempface: old_obj.Document.removeObject(tempface.Name) api_run( "void.add_opening", ifcfile, opening=opening, element=parent_element ) api_run("void.add_filling", ifcfile, opening=opening, element=element) # windows must also be part of a spatial container try: api_run("spatial.unassign_container", ifcfile, products=[element]) except: # old version of IfcOpenShell api_run("spatial.unassign_container", ifcfile, product=element) if parent_element.ContainedInStructure: container = parent_element.ContainedInStructure[0].RelatingStructure try: uprel = api_run( "spatial.assign_container", ifcfile, products=[element], relating_structure=container, ) except: # old version of IfcOpenShell uprel = api_run( "spatial.assign_container", ifcfile, product=element, relating_structure=container, ) elif parent_element.Decomposes: container = parent_element.Decomposes[0].RelatingObject try: uprel = api_run( "aggregate.assign_object", ifcfile, products=[element], relating_object=container, ) except: # older version of ifcopenshell uprel = api_run( "aggregate.assign_object", ifcfile, product=element, relating_object=container, ) # case 3: element aggregated inside other element else: try: api_run("aggregate.unassign_object", ifcfile, products=[element]) except: # older version of ifcopenshell api_run("aggregate.unassign_object", ifcfile, product=element) try: uprel = api_run( "aggregate.assign_object", ifcfile, products=[element], relating_object=parent_element, ) except: # older version of ifcopenshell uprel = api_run( "aggregate.assign_object", ifcfile, product=element, relating_object=parent_element, ) if hasattr(parent.Proxy, "addObject"): parent.Proxy.addObject(parent, obj) return uprel def get_elem_attribs(ifcentity): # This function can become pure IFC # usually info_ifcentity = ifcentity.get_info() would de the trick # the above could raise an unhandled exception on corrupted ifc files # in IfcOpenShell # see https://github.com/IfcOpenShell/IfcOpenShell/issues/2811 # thus workaround info_ifcentity = {"id": ifcentity.id(), "class": ifcentity.is_a()} # get attrib keys attribs = [] for anumber in range(20): try: attr = ifcentity.attribute_name(anumber) except Exception: break # print(attr) attribs.append(attr) # get attrib values for attr in attribs: try: value = getattr(ifcentity, attr) except Exception as e: # print(e) value = "Error: {}".format(e) print( "DEBUG: The entity #{} has a problem on attribute {}: {}".format( ifcentity.id(), attr, e ) ) # print(value) info_ifcentity[attr] = value return info_ifcentity def migrate_schema(ifcfile, schema): """migrates a file to a new schema""" # This function can become pure IFC newfile = ifcopenshell.file(schema=schema) migrator = ifcopenshell.util.schema.Migrator() table = {} for entity in ifcfile: new_entity = migrator.migrate(entity, newfile) table[entity.id()] = new_entity.id() return newfile, table def remove_ifc_element(obj): """removes the IFC data associated with an object""" # This function can become pure IFC ifcfile = get_ifcfile(obj) element = get_ifc_element(obj) if ifcfile and element: api_run("root.remove_product", ifcfile, product=element) return True return False def get_orphan_elements(ifcfile): """returns a list of orphan products in an ifcfile""" products = ifcfile.by_type("IfcElement") products = [p for p in products if not p.Decomposes] products = [p for p in products if not getattr(p, "ContainedInStructure", [])] products = [ p for p in products if not hasattr(p, "VoidsElements") or not p.VoidsElements ] return products def get_group(project, name): """returns a group of the given type under the given IFC project. Creates it if needed""" if not project: return None if hasattr(project, "Group"): group = project.Group elif hasattr(project, "Objects"): group = project.Objects else: group = [] for c in group: if c.isDerivedFrom("App::DocumentObjectGroupPython"): if c.Name == name: return c if hasattr(project, "Document"): doc = project.Document else: doc = project group = add_object(doc, otype="group", oname=name) group.Label = name.strip("Ifc").strip("Group") # if FreeCAD.GuiUp: # group.ViewObject.ShowInTree = PARAMS.GetBool("ShowDataGroups", False) if hasattr(project.Proxy, "addObject"): project.Proxy.addObject(project, group) return group def load_orphans(obj): """loads orphan objects from the given project object""" if isinstance(obj, FreeCAD.DocumentObject): doc = obj.Document else: doc = obj ifcfile = get_ifcfile(obj) shapemode = obj.ShapeMode elements = get_orphan_elements(ifcfile) for element in elements: create_object(element, doc, ifcfile, shapemode) def remove_tree(objs): """Removes all given objects and their children, if not used by others""" if not objs: return doc = objs[0].Document nobjs = objs for obj in objs: for child in obj.OutListRecursive: if not child in nobjs: nobjs.append(child) deletelist = [] for obj in nobjs: for par in obj.InList: if par not in nobjs: break else: deletelist.append(obj.Name) for n in deletelist: doc.removeObject(n)