# SPDX-License-Identifier: LGPL-2.1-or-later # *************************************************************************** # * * # * Copyright (c) 2022 Yorik van Havre * # * * # * This file is part of FreeCAD. * # * * # * FreeCAD is free software: you can redistribute it and/or modify it * # * under the terms of the GNU Lesser General Public License as * # * published by the Free Software Foundation, either version 2.1 of the * # * License, or (at your option) any later version. * # * * # * FreeCAD 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 * # * Lesser General Public License for more details. * # * * # * You should have received a copy of the GNU Lesser General Public * # * License along with FreeCAD. If not, see * # * . * # * * # *************************************************************************** """This is the main NativeIFC module""" import os from PySide import QtCore import FreeCAD import Arch import ArchBuildingPart import Draft from draftviewproviders import view_layer translate = FreeCAD.Qt.translate # heavyweight libraries - ifc_tools should always be lazy loaded try: import ifcopenshell import ifcopenshell.api import ifcopenshell.geom import ifcopenshell.util.attribute import ifcopenshell.util.element import ifcopenshell.util.placement import ifcopenshell.util.schema import ifcopenshell.util.unit import ifcopenshell.entity_instance except ImportError as e: import FreeCAD FreeCAD.Console.PrintError( translate( "BIM", "IfcOpenShell was not found on this system. IFC support is disabled", ) + "\n" ) raise e from . import ifc_objects from . import ifc_viewproviders from . import ifc_import from . import ifc_layers from . import ifc_status from . import ifc_export from . import ifc_psets 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 building 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 building 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 "Proxy" not in document.PropertiesList: document.addProperty("App::PropertyPythonObject", "Proxy", locked=True) 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 "IfcFilePath" not in proj.PropertiesList: proj.addProperty("App::PropertyFile", "IfcFilePath", "Base", d, locked=True) if "Modified" not in proj.PropertiesList: proj.addProperty("App::PropertyBool", "Modified", "Base", locked=True) 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 "Schema" not in proj.PropertiesList: proj.addProperty("App::PropertyEnumeration", "Schema", "Base", locked=True) # 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() org = param.GetString("prefCompany", "") person = None organisation = None if user: person = api_run("owner.add_person", ifcfile, family_name=user) 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]]) freecadorg = api_run( "owner.add_organisation", ifcfile, identification="FreeCAD.org", name="The FreeCAD project" ) application = api_run( "owner.add_application", ifcfile, application_developer=freecadorg, application_full_name=application, application_identifier=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 + sqm +degrees unit, as per # https://docs.ifcopenshell.org/autoapi/ifcopenshell/api/unit/index.html # TODO allow to set this at creation, from the current FreeCAD units schema length = api_run("unit.add_si_unit", ifcfile, unit_type="LENGTHUNIT") area = api_run("unit.add_si_unit", ifcfile, unit_type="AREAUNIT") angle = api_run("unit.add_conversion_based_unit", ifcfile, name="degree") api_run("unit.assign_unit", ifcfile, units=[length, area, angle]) # TODO add user history 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, objecttype=None): """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(), getattr(ifcentity, "Name", "") ) objecttype = ifc_export.get_object_type(ifcentity, objecttype) FreeCAD.Console.PrintLog(s) obj = add_object(document, otype=objecttype) 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") or ifcentity.is_a("IfcAnnotation") ): try: obj.ViewObject.DisplayMode = "Wireframe" except: pass 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 element.id() not 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) # TEST: mark new objects to recompute QtCore.QTimer.singleShot(0, lambda: recompute([get_object(c) for c in children])) return result def assign_groups(children, ifcfile=None): """Fill the groups in this list. Returns a list of processed FreeCAD objects""" result = [] 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, None, ifcfile) for rel in getattr(child, mode): for elem in rel.RelatedObjects: elobj = get_object(elem, None, ifcfile) 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 result.append(elobj) return result def get_children( obj, ifcfile=None, only_structure=False, assemblies=True, expand=False, ifctype=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 ifctype: result = [r for r in result if r.is_a(ifctype)] return result def get_freecad_children(obj): """Returns the children of this object that exist in the document""" objs = [] children = get_children(obj) for child in children: childobj = get_object(child) if childobj: objs.extend(get_freecad_children(childobj)) return objs def get_object(element, document=None, ifcfile=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, ifcfile) == 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 getattr(project, "IfcFilePath", None): 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 else: FreeCAD.Console.PrintError( "Error: No IFC file attached to this project: " + project.Label ) 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', 'text', 'dimension', 'sectionplane', 'axis', 'schedule' 'buildingpart' or anything else for a standard IFC object""" if not document: return None if otype == "schedule": obj = Arch.makeSchedule() elif otype == "sectionplane": obj = Arch.makeSectionPlane() obj.Proxy = ifc_objects.ifc_object(otype) elif otype == "axis": obj = Arch.makeAxis() obj.Proxy = ifc_objects.ifc_object(otype) obj.removeProperty("Angles") obj.removeProperty("Distances") obj.removeProperty("Labels") obj.removeProperty("Limit") if obj.ViewObject: obj.ViewObject.DisplayMode = "Flat Lines" elif otype == "dimension": obj = Draft.make_dimension(FreeCAD.Vector(), FreeCAD.Vector(1, 0, 0)) obj.Proxy = ifc_objects.ifc_object(otype) obj.removeProperty("Diameter") obj.removeProperty("Distance") obj.setPropertyStatus("LinkedGeometry", "Hidden") obj.setGroupOfProperty("Start", "Dimension") obj.setGroupOfProperty("End", "Dimension") obj.setGroupOfProperty("Direction", "Dimension") elif otype == "text": obj = Draft.make_text("") obj.Proxy = ifc_objects.ifc_object(otype) elif otype == "layer": proxy = ifc_objects.ifc_object(otype) obj = document.addObject("App::FeaturePython", oname, proxy, None, False) if obj.ViewObject: view_layer.ViewProviderLayer(obj.ViewObject) obj.ViewObject.addProperty("App::PropertyBool", "HideChildren", "Layer", locked=True) obj.ViewObject.HideChildren = True elif otype == "group": vproxy = ifc_viewproviders.ifc_vp_group() obj = document.addObject("App::DocumentObjectGroupPython", oname, None, vproxy, False) elif otype == "material": proxy = ifc_objects.ifc_object(otype) vproxy = ifc_viewproviders.ifc_vp_material() obj = document.addObject("App::MaterialObjectPython", oname, proxy, vproxy, False) elif otype == "project": proxy = ifc_objects.ifc_object(otype) vproxy = ifc_viewproviders.ifc_vp_document() obj = document.addObject("Part::FeaturePython", oname, proxy, vproxy, False) elif otype == "buildingpart": obj = Arch.makeBuildingPart() if obj.ViewObject: obj.ViewObject.ShowLevel = False obj.ViewObject.ShowLabel = False obj.ViewObject.Proxy = ifc_viewproviders.ifc_vp_buildingpart(obj.ViewObject) obj.ViewObject.Proxy.attach(obj.ViewObject) for p in obj.PropertiesList: if obj.getGroupOfProperty(p) in ["BuildingPart", "IFC Attributes", "Children"]: obj.removeProperty(p) obj.Proxy = ifc_objects.ifc_object(otype) else: # default case, standard IFC object proxy = ifc_objects.ifc_object(otype) vproxy = ifc_viewproviders.ifc_vp_object() obj = document.addObject("Part::FeaturePython", oname, proxy, vproxy, False) 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", locked=True) if "ShapeMode" not in obj.PropertiesList: obj.addProperty("App::PropertyEnumeration", "ShapeMode", "Base", locked=True) 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") if ifcentity.is_a("IfcProduct"): obj.addProperty("App::PropertyLink", "Type", "IFC", locked=True) 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", locked=True) 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", locked=True) 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", locked=True) 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", locked=True) setattr(obj, attr, value) elif data_type == "boolean": if attr not in obj.PropertiesList: obj.addProperty("App::PropertyBool", attr, "IFC", locked=True) 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: obj.addProperty("App::PropertyLink", attr, "IFC", locked=True) elif isinstance(value, (list, tuple)) and value: if isinstance(value[0], ifcopenshell.entity_instance): if links: if attr not in obj.PropertiesList: obj.addProperty("App::PropertyLinkList", attr, "IFC", locked=True) elif data_type == "enum": if attr not in obj.PropertiesList: obj.addProperty("App::PropertyEnumeration", attr, "IFC", locked=True) 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) elif attr in ["RefLongitude", "RefLatitude"]: obj.addProperty("App::PropertyFloat", attr, "IFC", locked=True) if value is not None: # convert from list of 4 ints value = value[0] + value[1] / 60.0 + value[2] / 3600.0 + value[3] / 3600.0e6 setattr(obj, attr, value) else: if attr not in obj.PropertiesList: obj.addProperty("App::PropertyString", attr, "IFC", locked=True) if value is not None: setattr(obj, attr, str(value)) # We shortly go through the list of IFCRELASSOCIATESCLASSIFICATION members # in the file to see if the newly added object should have a Classification added # since we can run `add_properties`, when changing from IFC Object to IFC Type, or BIM Object (Standard Code) # to BIM Type, and during the process of creation the only place where we save Classification is # the file itself, so below code retrieves it and assigns it back to the newly created obj. if not hasattr(obj, "Classification"): assoc_classifications = ifcfile.by_type("IfcRelAssociatesClassification") for assoc in assoc_classifications: related_objects = assoc.RelatedObjects if isinstance(related_objects, ifcopenshell.entity_instance): related_objects = [related_objects] if ifcentity in related_objects: cref = assoc.RelatingClassification if cref and cref.is_a("IfcClassificationReference"): classification_name = "" # Try to get the source classification name if hasattr(cref, "ReferencedSource") and cref.ReferencedSource: if ( hasattr(cref.ReferencedSource, "Name") and cref.ReferencedSource.Name ): classification_name += cref.ReferencedSource.Name + " " # Add the Identification if present if cref.Identification: classification_name += cref.Identification classification_name = classification_name.strip() if classification_name: obj.addProperty( "App::PropertyString", "Classification", "IFC", locked=True ) setattr(obj, "Classification", classification_name) break # Found the relevant one, stop # annotation properties if ifcentity.is_a("IfcGridAxis"): axisdata = ifc_export.get_axis(ifcentity) if axisdata: if "Placement" not in obj.PropertiesList: obj.addProperty("App::PropertyPlacement", "Placement", "Base", locked=True) if "CustomText" in obj.PropertiesList: obj.setPropertyStatus("CustomText", "Hidden") obj.setExpression("CustomText", "AxisTag") if "Length" not in obj.PropertiesList: obj.addProperty("App::PropertyLength", "Length", "Axis", locked=True) if "Text" not in obj.PropertiesList: obj.addProperty("App::PropertyStringList", "Text", "Base", locked=True) obj.Placement = axisdata[0] obj.Length = axisdata[1] # axisdata[2] is the axis tag, it is already applied by other code elif ifcentity.is_a("IfcAnnotation"): sectionplane = ifc_export.get_sectionplane(ifcentity) if sectionplane: if "Placement" not in obj.PropertiesList: obj.addProperty("App::PropertyPlacement", "Placement", "Base", locked=True) if "Depth" not in obj.PropertiesList: obj.addProperty("App::PropertyLength", "Depth", "SectionPlane", locked=True) obj.Placement = sectionplane[0] if len(sectionplane) > 3: obj.Depth = sectionplane[3] vobj = obj.ViewObject if vobj: if "DisplayLength" not in vobj.PropertiesList: vobj.addProperty( "App::PropertyLength", "DisplayLength", "SectionPlane", locked=True ) if "DisplayHeight" not in vobj.PropertiesList: vobj.addProperty( "App::PropertyLength", "DisplayHeight", "SectionPlane", locked=True ) if len(sectionplane) > 1: vobj.DisplayLength = sectionplane[1] if len(sectionplane) > 2: vobj.DisplayHeight = sectionplane[2] else: dim = ifc_export.get_dimension(ifcentity) if dim and len(dim) >= 3: if "Start" not in obj.PropertiesList: obj.addProperty("App::PropertyVectorDistance", "Start", "Base", locked=True) if "End" not in obj.PropertiesList: obj.addProperty("App::PropertyVectorDistance", "End", "Base", locked=True) if "Dimline" not in obj.PropertiesList: obj.addProperty("App::PropertyVectorDistance", "Dimline", "Base", locked=True) obj.Start = dim[1] obj.End = dim[2] if len(dim) > 3: obj.Dimline = dim[3] else: mid = obj.End.sub(obj.Start) mid.multiply(0.5) obj.Dimline = obj.Start.add(mid) else: text = ifc_export.get_text(ifcentity) if text: if "Placement" not in obj.PropertiesList: obj.addProperty("App::PropertyPlacement", "Placement", "Base", locked=True) if "Text" not in obj.PropertiesList: obj.addProperty("App::PropertyStringList", "Text", "Base", locked=True) obj.Text = [text.Literal] obj.Placement = ifc_export.get_placement(ifcentity.ObjectPlacement, ifcfile) elif ifcentity.is_a("IfcControl"): ifc_psets.show_psets(obj) # 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")] 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 def differs(val1, val2): if val1 == val2: return False if not val1 and not val2: return False if isinstance(val1, (tuple, list)): if tuple(val1) == tuple(val2): return False if val1 is None and "NOTDEFINED" in str(val2).upper(): return False if val1 is None and "UNDEFINED" in str(val2).upper(): return False if val2 is None and "NOTDEFINED" in str(val1).upper(): return False if val2 is None and "UNDEFINED" in str(val1).upper(): return False return True 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 if attribute in ["RefLongitude", "RefLatitude"]: c = [int(value)] c.append(int((value - c[0]) * 60)) c.append(int(((value - c[0]) * 60 - c[1]) * 60)) c.append(int((((value - c[0]) * 60 - c[1]) * 60 - c[2]) * 1.0e6)) value = c 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 differs(getattr(element, attribute, None), value): FreeCAD.Console.PrintLog( "Changing IFC attribute value of " + str(attribute) + ": " + str(value) + " (original value:" + str(getattr(element, attribute)) + ")" + "\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"): # 1.0 materials if not isinstance(colors[0], (tuple, list)): colors = [colors] # set the first color to opaque otherwise it spoils object transparency if len(colors) > 1: # TEMP HACK: if multiple colors, set everything to opaque because it looks wrong colors = [color[:3] + (1.0,) for color in colors] sapp = [] for color in colors: sapp_mat = FreeCAD.Material() if len(color) < 4: sapp_mat.DiffuseColor = color + (1.0,) else: sapp_mat.DiffuseColor = color[:3] + (1.0 - color[3],) sapp_mat.Transparency = 1.0 - color[3] if len(color) > 3 else 0.0 sapp.append(sapp_mat) vobj.ShapeAppearance = sapp 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) if not hasattr(element, "ObjectPlacement"): # special case: this is a grid axis, it has no placement if element.is_a("IfcGridAxis"): return set_axis_points(obj, element, ifcfile) # other cases of objects without ObjectPlacement? print("DEBUG: object without ObjectPlacement", element) return False 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 set_axis_points(obj, element, ifcfile): """Sets the points of an axis from placement and length""" if element.AxisCurve.is_a("IfcPolyline"): p1 = obj.Placement.Base p2 = obj.Placement.multVec(FreeCAD.Vector(0, obj.Length.Value, 0)) api_run( "attribute.edit_attributes", ifcfile, product=element.AxisCurve.Points[0], attributes={"Coordinates": tuple(p1)}, ) api_run( "attribute.edit_attributes", ifcfile, product=element.AxisCurve.Points[-1], attributes={"Coordinates": tuple(p2)}, ) return True print("DEBUG: unhandled axis type:", element.AxisCurve.is_a()) 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, mode=None): """Takes any FreeCAD object and aggregates it to an existing IFC object. Mode can be 'opening' to force-create a subtraction""" 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) if not ifcfile: return product = None objecttype = None new = False 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 else: ifcclass = None if mode == "opening": ifcclass = "IfcOpeningElement" if ifc_export.is_annotation(obj): product = ifc_export.create_annotation(obj, ifcfile) if Draft.get_type(obj) in ["DraftText", "Text"]: objecttype = "text" elif "CreateSpreadsheet" in obj.PropertiesList: obj.Proxy.create_ifc(obj, ifcfile) newobj = obj else: product = ifc_export.create_product(obj, parent, ifcfile, ifcclass) if product: exobj = get_object(product, obj.Document) if exobj is None: shapemode = getattr(parent, "ShapeMode", DEFAULT_SHAPEMODE) newobj = create_object(product, obj.Document, ifcfile, shapemode, objecttype) new = True else: newobj = exobj create_relationship(obj, newobj, parent, product, ifcfile, mode) 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: aggregate(child, newobj) for child in getattr(obj, "Group", []): if newobj.IfcClass == "IfcGroup" and child in obj.Group: 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 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"]: return "IfcGroup" return "IfcBuildingElementProxy" 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, mode=None): """Creates a relationship between an IFC object and a parent IFC object""" if isinstance(parent, (FreeCAD.DocumentObject, FreeCAD.Document)): parent_element = get_ifc_element(parent) else: parent_element = parent uprel = None # case 4: anything inside group if parent_element.is_a("IfcGroup"): # special case: adding a section plane to a grouo turns it into a drawing # and removes it from any containment if element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING": parent.ObjectType = "DRAWING" try: api_run("spatial.unassign_container", ifcfile, products=[parent_element]) except: # older version of IfcOpenShell api_run("spatial.unassign_container", ifcfile, product=parent_element) # IFC objects can be part of multiple groups but we do the FreeCAD way here # and remove from any previous group for assignment in getattr(element, "HasAssignments", []): if assignment.is_a("IfcRelAssignsToGroup"): if element in assignment.RelatedObjects: oldgroup = assignment.RelatingGr try: api_run("group.unassign_group", ifcfile, products=[element], group=oldgroup) except: # older version of IfcOpenShell api_run("group.unassign_group", ifcfile, product=element, group=oldgroup) try: uprel = api_run("group.assign_group", ifcfile, products=[element], group=parent_element) except: # older version of IfcOpenShell uprel = api_run("group.assign_group", ifcfile, product=element, group=parent_element) # case 1: element inside spatiual structure elif parent_element.is_a("IfcSpatialStructureElement") and element.is_a("IfcElement"): # first remove the FreeCAD object from any parent if old_obj: 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: uprel = api_run("spatial.unassign_container", ifcfile, products=[element]) except: # older version of IfcOpenShell uprel = 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: door/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", ]: if old_obj: tempface, tempobj = get_subvolume(old_obj) if tempobj: opening = ifc_export.create_product(tempobj, parent, ifcfile, "IfcOpeningElement") set_attribute(ifcfile, opening, "Name", "Opening") 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 4: void element elif (parent_element.is_a("IfcElement") and element.is_a("IfcOpeningElement")) or ( mode == "opening" ): uprel = api_run("void.add_opening", ifcfile, opening=element, element=parent_element) # case 3: element aggregated inside other element elif element.is_a("IfcProduct"): 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") and 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 attribs.append(attr) # get attrib values for attr in attribs: try: value = getattr(ifcentity, attr) except Exception as e: value = "Error: {}".format(e) print( "DEBUG: The entity #{} has a problem on attribute {}: {}".format( ifcentity.id(), attr, e ) ) 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, delete_obj=False): """removes the IFC data associated with an object. If delete_obj is True, the FreeCAD object is also deleted""" # 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) if delete_obj: obj.Document.removeObject(obj.Name) return True return False def get_orphan_elements(ifcfile): """returns a list of orphan products in an ifcfile""" products = ifcfile.by_type("IfcProduct") 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] # add control elements proj = ifcfile.by_type("IfcProject")[0] for rel in getattr(proj, "Declares", []): for ctrl in getattr(rel, "RelatedDefinitions", []): if ctrl.is_a("IfcControl"): products.append(ctrl) groups = [] for o in products: for rel in getattr(o, "HasAssignments", []): if rel.is_a("IfcRelAssignsToGroup"): g = rel.RelatingGroup if (g not in products) and (g not in groups): groups.append(g) products.extend(groups) 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 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) objs = [] for element in elements: nobj = create_object(element, doc, ifcfile, shapemode) objs.append(nobj) processed = assign_groups(elements, ifcfile) # put things under project. This is important so orphan elements still can find # their IFC file rest = [o for o in objs if o not in processed] if rest: project = get_project(ifcfile) if isinstance(project, FreeCAD.DocumentObject): for o in rest: project.Proxy.addObject(project, o) # TEST: Try recomputing QtCore.QTimer.singleShot(0, lambda: recompute(objs)) 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 child not 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) def recompute(children): """Temporary function to recompute objects. Some objects don't get their shape correctly at creation""" doc = None for c in children: if c: c.touch() doc = c.Document if doc: doc.recompute()