# *************************************************************************** # * Copyright (c) 2014 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 * # * * # *************************************************************************** """Provide the importer for IFC files used above all in Arch and BIM. Internally it uses IfcOpenShell, which must be installed before using. """ ## @package importIFC # \ingroup ARCH # \brief IFC file format importer # # This module provides tools to import IFC files. from __future__ import print_function import six import os import math import time import FreeCAD import Part import Draft import Arch import DraftVecUtils import ArchIFCSchema import importIFCHelper import importIFCmulticore from draftutils.messages import _msg, _err if FreeCAD.GuiUp: import FreeCADGui as Gui __title__ = "FreeCAD IFC importer - Enhanced IfcOpenShell-only version" __author__ = ("Yorik van Havre", "Jonathan Wiedemann", "Bernd Hahnebach") __url__ = "https://www.freecadweb.org" DEBUG = False # Set to True to see debug messages. Otherwise, totally silent ZOOMOUT = True # Set to False to not zoom extents after import # Save the Python open function because it will be redefined if open.__module__ in ['__builtin__', 'io']: pyopen = open # Templates and other definitions **** # which IFC type must create which FreeCAD type typesmap = { "Site": [ "IfcSite" ], "Building": [ "IfcBuilding" ], "Floor": [ "IfcBuildingStorey" ], "Structure": [ "IfcBeam", "IfcBeamStandardCase", "IfcColumn", "IfcColumnStandardCase", "IfcSlab", "IfcFooting", "IfcPile", "IfcTendon" ], "Wall": [ "IfcWall", "IfcWallStandardCase", "IfcCurtainWall" ], "Window": [ "IfcWindow", "IfcWindowStandardCase", "IfcDoor", "IfcDoorStandardCase" ], "Roof": [ "IfcRoof" ], "Stairs": [ "IfcStair", "IfcStairFlight", "IfcRamp", "IfcRampFlight" ], "Space": [ "IfcSpace" ], "Rebar": [ "IfcReinforcingBar" ], "Panel": [ "IfcPlate" ], "Equipment": [ "IfcFurnishingElement", "IfcSanitaryTerminal", "IfcFlowTerminal", "IfcElectricAppliance" ], "Pipe": [ "IfcPipeSegment" ], "PipeConnector": [ "IfcPipeFitting" ], "BuildingPart": [ "IfcElementAssembly" ] } # which IFC entity (product) is a structural object structuralifcobjects = ( "IfcStructuralCurveMember", "IfcStructuralSurfaceMember", "IfcStructuralPointConnection", "IfcStructuralCurveConnection", "IfcStructuralSurfaceConnection", "IfcStructuralAction", "IfcStructuralPointAction", "IfcStructuralLinearAction", "IfcStructuralLinearActionVarying", "IfcStructuralPlanarAction" ) def getPreferences(): """Retrieve the IFC preferences available in import and export. MERGE_MODE_ARCH: 0 = parametric arch objects 1 = non-parametric arch objects 2 = Part shapes 3 = One compound per storey """ p = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Arch") if FreeCAD.GuiUp and p.GetBool("ifcShowDialog", False): Gui.showPreferences("Import-Export", 0) preferences = { 'DEBUG': p.GetBool("ifcDebug", False), 'PREFIX_NUMBERS': p.GetBool("ifcPrefixNumbers", False), 'SKIP': p.GetString("ifcSkip", "").split(","), 'SEPARATE_OPENINGS': p.GetBool("ifcSeparateOpenings", False), 'ROOT_ELEMENT': p.GetString("ifcRootElement", "IfcProduct"), 'GET_EXTRUSIONS': p.GetBool("ifcGetExtrusions", False), 'MERGE_MATERIALS': p.GetBool("ifcMergeMaterials", False), 'MERGE_MODE_ARCH': p.GetInt("ifcImportModeArch", 0), 'MERGE_MODE_STRUCT': p.GetInt("ifcImportModeStruct", 1), 'CREATE_CLONES': p.GetBool("ifcCreateClones", True), 'IMPORT_PROPERTIES': p.GetBool("ifcImportProperties", False), 'SPLIT_LAYERS': p.GetBool("ifcSplitLayers", False), # wall layer, not layer for visual props 'FITVIEW_ONIMPORT': p.GetBool("ifcFitViewOnImport", False), 'ALLOW_INVALID': p.GetBool("ifcAllowInvalid", False), 'REPLACE_PROJECT': p.GetBool("ifcReplaceProject", False), 'MULTICORE': p.GetInt("ifcMulticore", 0), 'IMPORT_LAYER': p.GetBool("ifcImportLayer", True) } if preferences['MERGE_MODE_ARCH'] > 0: preferences['SEPARATE_OPENINGS'] = False preferences['GET_EXTRUSIONS'] = False if not preferences['SEPARATE_OPENINGS']: preferences['SKIP'].append("IfcOpeningElement") return preferences def export(exportList, filename, colors=None, preferences=None): """Export the selected objects to IFC format. The export code is now in a separate module; this call is retained in this module for compatibility purposes with older scripts. """ import exportIFC exportIFC.export(exportList, filename, colors, preferences) def open(filename, skip=[], only=[], root=None): """Open an IFC file inside a new document. TODO: change the default argument to `None`, instead of `[]`. This is better because lists are mutable. Most of the work is done in the `insert` function. """ docname = os.path.splitext(os.path.basename(filename))[0] docname = importIFCHelper.decode(docname, utf=True) doc = FreeCAD.newDocument(docname) doc.Label = docname doc = insert(filename, doc.Name, skip, only, root) return doc def insert(srcfile, docname, skip=[], only=[], root=None, preferences=None): """Import the contents of an IFC file in the current active document. TODO: change the default argument to `None`, instead of `[]`. This is better because lists are mutable. Parameters ---------- skip: list By default empty list `[]`. Can contain a list of ids of objects to be skipped. only: list By default, empty list `[]`. Restrict the import to certain object ids; it will also get their children. root: object It is used to import only the derivates of a certain element type, for example, `'ifcProduct' """ try: import ifcopenshell from ifcopenshell import geom # Sometimes there is an error importing `geom` in this way # import ifcopenshell.geom # # therefore we must use the `from x import y` way. # # For some reason this works; see the bug report # https://github.com/IfcOpenShell/IfcOpenShell/issues/689 except ModuleNotFoundError: _err("IfcOpenShell was not found on this system. " "IFC support is disabled.\n" "Visit https://wiki.freecadweb.org/IfcOpenShell " "to learn about installing it.") return starttime = time.time() # in seconds if preferences is None: preferences = getPreferences() if preferences["MULTICORE"] and not hasattr(srcfile, "by_guid"): return importIFCmulticore.insert(srcfile, docname, preferences) try: doc = FreeCAD.getDocument(docname) except NameError: doc = FreeCAD.newDocument(docname) FreeCAD.ActiveDocument = doc global parametrics # allow to override the root element if root: preferences['ROOT_ELEMENT'] = root # keeping global variable for debugging purposes # global ifcfile # If the `by_guid` attribute exists, this is already a loaded ifcfile, # otherwise, it's just a string, and we have to open it with ifcopenshell if hasattr(srcfile, "by_guid"): ifcfile = srcfile filesize = None filename = None else: if preferences['DEBUG']: _msg("Opening '{}'... ".format(srcfile), end="") filename = importIFCHelper.decode(srcfile, utf=True) filesize = os.path.getsize(filename) * 1E-6 # in megabytes ifcfile = ifcopenshell.open(filename) if preferences['DEBUG']: _msg("done.") # Get file scale ifcscale = importIFCHelper.getScaling(ifcfile) # IfcOpenShell multiplies the precision value of the file by 100 # So we raise the precision by 100 too to compensate. # ctxs = ifcfile.by_type("IfcGeometricRepresentationContext") # for ctx in ctxs: # if not ctx.is_a("IfcGeometricRepresentationSubContext"): # ctx.Precision = ctx.Precision/100 # Set default ifcopenshell options to work in brep mode settings = geom.settings() settings.set(settings.USE_BREP_DATA, True) settings.set(settings.SEW_SHELLS, True) settings.set(settings.USE_WORLD_COORDS, True) if preferences['SEPARATE_OPENINGS']: settings.set(settings.DISABLE_OPENING_SUBTRACTIONS, True) if preferences['SPLIT_LAYERS'] and hasattr(settings, "APPLY_LAYERSETS"): settings.set(settings.APPLY_LAYERSETS, True) # build all needed tables if preferences['DEBUG']: _msg("Building types and relationships table...") # type tables sites = ifcfile.by_type("IfcSite") buildings = ifcfile.by_type("IfcBuilding") floors = ifcfile.by_type("IfcBuildingStorey") openings = ifcfile.by_type("IfcOpeningElement") materials = ifcfile.by_type("IfcMaterial") (products, annotations) = importIFCHelper.buildRelProductsAnnotations(ifcfile, preferences['ROOT_ELEMENT']) # empty relation tables objects = {} # { id:object, ... } shapes = {} # { id:shaoe } only used for merge mode structshapes = {} # { id:shaoe } only used for merge mode sharedobjects = {} # { representationmapid:object } # a list of imported objects whose parametric relationships # need processing after all objects have been created parametrics = [] profiles = {} # to store reused extrusion profiles {ifcid:fcobj, ...} layers = {} # { layer_name, [ids] } # filled relation tables # TODO: investigate using inverse attributes. # For the following tables it might be better to use inverse attributes # to find the properties, otherwise a lot of loops # and if testing is needed. # See https://forum.freecadweb.org/viewtopic.php?f=39&t=37892 prodrepr = importIFCHelper.buildRelProductRepresentation(ifcfile) additions = importIFCHelper.buildRelAdditions(ifcfile) groups = importIFCHelper.buildRelGroups(ifcfile) subtractions = importIFCHelper.buildRelSubtractions(ifcfile) mattable = importIFCHelper.buildRelMattable(ifcfile) colors = importIFCHelper.buildRelProductColors(ifcfile, prodrepr) colordict = {} # { objname:color tuple } for non-GUI use if preferences['DEBUG']: _msg("done.") # only import a list of IDs and their children, if defined if only: ids = [] while only: currentid = only.pop() ids.append(currentid) if currentid in additions.keys(): only.extend(additions[currentid]) products = [ifcfile[currentid] for currentid in ids] # start the actual import, set FreeCAD UI count = 0 from FreeCAD import Base progressbar = Base.ProgressIndicator() progressbar.start("Importing IFC objects...", len(products)) if preferences['DEBUG']: _msg("Parsing {} BIM objects...".format(len(products))) # Prepare the 3D view if applicable if preferences['FITVIEW_ONIMPORT'] and FreeCAD.GuiUp: overallboundbox = None Gui.ActiveDocument.activeView().viewAxonometric() # Create the base project object if not preferences['REPLACE_PROJECT']: if len(ifcfile.by_type("IfcProject")) > 0: projectImporter = importIFCHelper.ProjectImporter(ifcfile, objects) projectImporter.execute() else: # https://forum.freecadweb.org/viewtopic.php?f=39&t=40624 print("No IfcProject found in the ifc file. Nothing imported") return doc # handle IFC products for product in products: count += 1 pid = product.id() guid = product.GlobalId ptype = product.is_a() if preferences['DEBUG']: print(count,"/",len(products),"object #"+str(pid),":",ptype,end="") # build list of related property sets psets = importIFCHelper.getIfcPropertySets(ifcfile, pid) # add layer names to layers if hasattr(product, "Representation") and hasattr(product.Representation, "Representations"): if len(product.Representation.Representations) > 0: lays = product.Representation.Representations[0].LayerAssignments if len(lays) > 0: layer_name = lays[0].Name if layer_name not in list(layers.keys()): layers[layer_name] = [pid] else: layers[layer_name].append(pid) if preferences['DEBUG']: print(" layer ", layer_name, " found", ptype,end="") else: if preferences['DEBUG']: print(" no layer found", ptype,end="") # checking for full FreeCAD parametric definition, overriding everything else if psets and FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Arch").GetBool("IfcImportFreeCADProperties",False): if "FreeCADPropertySet" in [ifcfile[pset].Name for pset in psets.keys()]: if preferences['DEBUG']: print(" restoring from parametric definition...",end="") obj,parametrics = importIFCHelper.createFromProperties(psets,ifcfile,parametrics) if obj: objects[pid] = obj if preferences['DEBUG']: print("done") continue else: if preferences['DEBUG']: print("failed") # no parametric data, we go the good old way name = str(ptype[3:]) if product.Name: name = product.Name if six.PY2: name = name.encode("utf8") if preferences['PREFIX_NUMBERS']: name = "ID" + str(pid) + " " + name obj = None baseobj = None brep = None shape = None # classify object and verify if we must skip it archobj = True # assume all objects not in structuralifcobjects are architecture structobj = False if ptype in structuralifcobjects: archobj = False structobj = True if preferences['DEBUG']: print(" (struct)",end="") else: if preferences['DEBUG']: print(" (arch)",end="") if preferences['MERGE_MODE_ARCH'] == 4 and archobj: if preferences['DEBUG']: print(" skipped.") continue if preferences['MERGE_MODE_STRUCT'] == 3 and not archobj: if preferences['DEBUG']: print(" skipped.") continue if pid in skip: # user given id skip list if preferences['DEBUG']: print(" skipped.") continue if ptype in skip: # user given type skip list if preferences['DEBUG']: print(" skipped.") continue if ptype in preferences['SKIP']: # preferences-set type skip list if preferences['DEBUG']: print(" skipped.") continue if preferences['REPLACE_PROJECT']: # options-enabled project/site/building skip if ptype in ['IfcProject','IfcSite']: if preferences['DEBUG']: print(" skipped.") continue elif ptype in ['IfcBuilding']: if len(ifcfile.by_type("IfcBuilding")) == 1: # let multiple buildings through... if preferences['DEBUG']: print(" skipped.") continue elif ptype in ['IfcBuildingStorey']: if len(ifcfile.by_type("IfcBuildingStorey")) == 1: # let multiple storeys through... if preferences['DEBUG']: print(" skipped.") continue # check if this object is sharing its shape (mapped representation) clone = None store = None prepr = None try: prepr = product.Representation except Exception: if preferences['DEBUG']: print(" ERROR unable to get object representation",end="") if prepr and (preferences['MERGE_MODE_ARCH'] == 0) and archobj and preferences['CREATE_CLONES']: for r in prepr.Representations: if r.RepresentationIdentifier.upper() == "BODY": if r.Items[0].is_a("IfcMappedItem"): originalid = r.Items[0].MappingSource.id() if originalid in sharedobjects: clone = sharedobjects[originalid] else: sharedobjects[originalid] = None store = originalid # flag this object to be stored later # set additional setting for structural entities if hasattr(settings,"INCLUDE_CURVES"): if structobj: settings.set(settings.INCLUDE_CURVES,True) else: settings.set(settings.INCLUDE_CURVES,False) try: cr = geom.create_shape(settings, product) brep = cr.geometry.brep_data except Exception: pass # IfcOpenShell will yield an error if a given product has no shape, but we don't care, we're brave enough # from now on we have a brep string if brep: if preferences['DEBUG']: print(" "+str(int(len(brep)/1000))+"k ",end="") # create a Part shape shape = Part.Shape() shape.importBrepFromString(brep,False) shape.scale(1000.0) # IfcOpenShell always outputs in meters, we convert to mm, the freecad internal unit if shape.isNull() and (not preferences['ALLOW_INVALID']): if preferences['DEBUG']: print("null shape ",end="") elif not shape.isValid() and (not preferences['ALLOW_INVALID']): if preferences['DEBUG']: print("invalid shape ",end="") else: # add to the global boundbox if applicable if preferences['FITVIEW_ONIMPORT'] and FreeCAD.GuiUp: try: bb = shape.BoundBox # if preferences['DEBUG']: print(' ' + str(bb),end="") except Exception: bb = None if preferences['DEBUG']: print(' BB could not be computed',end="") if bb and bb.isValid(): if not overallboundbox: overallboundbox = bb if not overallboundbox.isInside(bb): Gui.SendMsgToActiveView("ViewFit") overallboundbox.add(bb) if (preferences['MERGE_MODE_ARCH'] > 0 and archobj) or structobj: # we are not using Arch objects # additional tweaks to set when not using Arch objects if ptype == "IfcSpace": # do not add spaces to compounds if preferences['DEBUG']: print("skipping space ",pid,end="") elif structobj: structshapes[pid] = shape if preferences['DEBUG']: print(len(shape.Solids),"solids ",end="") baseobj = shape else: shapes[pid] = shape if preferences['DEBUG']: print(len(shape.Solids),"solids ",end="") baseobj = shape else: # create base shape object if clone: if preferences['DEBUG']: print("clone ",end="") else: if preferences['GET_EXTRUSIONS'] and (preferences['MERGE_MODE_ARCH'] != 1): # get IFC profile profileid = None sortmethod = None if product.Representation: if product.Representation.Representations: if product.Representation.Representations[0].is_a("IfcShapeRepresentation"): if product.Representation.Representations[0].Items: if product.Representation.Representations[0].Items[0].is_a("IfcExtrudedAreaSolid"): profileid = product.Representation.Representations[0].Items[0].SweptArea.id() sortmethod = importIFCHelper.getProfileCenterPoint(product.Representation.Representations[0].Items[0]) # recompose extrusions from a shape if not sortmethod: if ptype in ["IfcWall","IfcWallStandardCase","IfcSpace"]: sortmethod = "z" else: sortmethod = "area" ex = Arch.getExtrusionData(shape,sortmethod) # is this an extrusion? if ex: # check for extrusion profile baseface = None addplacement = None if profileid and (profileid in profiles): # reuse existing profile if existing print("shared extrusion ",end="") baseface = profiles[profileid] # calculate delta placement between stored profile and this one addplacement = FreeCAD.Placement() r = FreeCAD.Rotation(baseface.Shape.Faces[0].normalAt(0,0),ex[0].Faces[0].normalAt(0,0)) if r.Angle > 0.000001: # use shape methods to easily obtain a correct placement ts = Part.Shape() ts.rotate(DraftVecUtils.tup(baseface.Shape.CenterOfMass), DraftVecUtils.tup(r.Axis), math.degrees(r.Angle)) addplacement = ts.Placement d = ex[0].CenterOfMass.sub(baseface.Shape.CenterOfMass) if d.Length > 0.000001: addplacement.move(d) if not baseface: # this is an extrusion but we haven't built the profile yet print("extrusion ",end="") import DraftGeomUtils if DraftGeomUtils.hasCurves(ex[0]) or len(ex[0].Wires) != 1: # is this a circle? if (len(ex[0].Edges) == 1) and isinstance(ex[0].Edges[0].Curve,Part.Circle): baseface = Draft.makeCircle(ex[0].Edges[0]) else: # curves or holes? We just make a Part face baseface = doc.addObject("Part::Feature",name+"_footprint") # bug/feature in ifcopenshell? Some faces of a shell may have non-null placement # workaround to remove the bad placement: exporting/reimporting as step if not ex[0].Placement.isNull(): import tempfile fd, tf = tempfile.mkstemp(suffix=".stp") ex[0].exportStep(tf) f = Part.read(tf) os.close(fd) os.remove(tf) else: f = ex[0] baseface.Shape = f else: # no curve and no hole, we can make a draft object verts = [v.Point for v in ex[0].Wires[0].OrderedVertexes] # TODO verts are different if shape is made of RectangleProfileDef or not # is this a rectangle? if importIFCHelper.isRectangle(verts): baseface = Draft.makeRectangle(verts,face=True) else: # no hole and no curves, we make a Draft Wire instead baseface = Draft.makeWire(verts,closed=True) if profileid: # store for possible shared use profiles[profileid] = baseface baseobj = doc.addObject("Part::Extrusion",name+"_body") baseobj.Base = baseface if addplacement: # apply delta placement (stored profile) baseobj.Placement = addplacement baseobj.Dir = addplacement.Rotation.inverted().multVec(ex[1]) else: baseobj.Dir = ex[1] if FreeCAD.GuiUp: baseface.ViewObject.hide() if (not baseobj): baseobj = doc.addObject("Part::Feature",name+"_body") baseobj.Shape = shape else: # this object has no shape (storeys, etc...) if preferences['DEBUG']: print(" no brep ",end="") # we now have the shape, we create the final object if preferences['MERGE_MODE_ARCH'] == 0 and archobj: # full Arch objects for freecadtype,ifctypes in typesmap.items(): if ptype not in ifctypes: continue if clone: obj = getattr(Arch,"make"+freecadtype)(name=name) obj.CloneOf = clone # calculate the correct distance from the cloned object if shape: if shape.Solids: s1 = shape.Solids[0] else: s1 = shape if clone.Shape.Solids: s2 = clone.Shape.Solids[0] else: s1 = clone.Shape if hasattr(s1,"CenterOfMass") and hasattr(s2,"CenterOfMass"): v = s1.CenterOfMass.sub(s2.CenterOfMass) if product.Representation: r = importIFCHelper.getRotation(product.Representation.Representations[0].Items[0].MappingTarget) if not r.isNull(): v = v.add(s2.CenterOfMass) v = v.add(r.multVec(s2.CenterOfMass.negative())) obj.Placement.Rotation = r obj.Placement.move(v) else: print("failed to compute placement ",) else: # no clone obj = getattr(Arch,"make"+freecadtype)(baseobj=baseobj,name=name) if freecadtype in ["Wall","Structure"] and baseobj and baseobj.isDerivedFrom("Part::Extrusion"): # remove intermediary extrusion for types that can extrude themselves # TODO do this check earlier so we do not calculate it, to save time obj.Base = baseobj.Base obj.Placement = obj.Placement.multiply(baseobj.Placement) obj.Height = baseobj.Dir.Length obj.Normal = FreeCAD.Vector(baseobj.Dir).normalize() bn = baseobj.Name doc.removeObject(bn) if (freecadtype in ["Structure","Wall"]) and not baseobj: # remove sizes to prevent auto shape creation for types that don't require a base object obj.Height = 0 obj.Width = 0 obj.Length = 0 if store: sharedobjects[store] = obj # set the placement from the storey's elevation property if ptype == "IfcBuildingStorey": if product.Elevation: obj.Placement.Base.z = product.Elevation * ifcscale break if not obj: # we couldn't make an object of a specific type, use default arch component obj = Arch.makeComponent(baseobj,name=name) # set additional properties obj.Label = name if preferences['DEBUG']: print(": "+obj.Label+" ",end="") if hasattr(obj,"Description") and hasattr(product,"Description"): if product.Description: obj.Description = product.Description if FreeCAD.GuiUp and baseobj: try: if hasattr(baseobj,"ViewObject"): baseobj.ViewObject.hide() except ReferenceError: pass # setting IFC type try: if hasattr(obj,"IfcType"): obj.IfcType = ''.join(map(lambda x: x if x.islower() else " "+x, ptype[3:]))[1:] except Exception: print("Unable to give IFC type ",ptype," to object ",obj.Label) # setting uid if hasattr(obj,"IfcData"): a = obj.IfcData a["IfcUID"] = str(guid) obj.IfcData = a # setting IFC attributes for attribute in ArchIFCSchema.IfcProducts[product.is_a()]["attributes"]: if attribute["name"] == "Name": continue # print("attribute:",attribute["name"]) if hasattr(product, attribute["name"]) and getattr(product, attribute["name"]) and hasattr(obj,attribute["name"]): # print("Setting attribute",attribute["name"],"to",getattr(product, attribute["name"])) setattr(obj, attribute["name"], getattr(product, attribute["name"])) # TODO: ArchIFCSchema.IfcProducts uses the IFC version from the FreeCAD prefs. # This might not coincide with the file being opened, hence some attributes are not properly read. if obj: # print number of solids if preferences['DEBUG']: s = "" if hasattr(obj,"Shape"): if obj.Shape.Solids: s = str(len(obj.Shape.Solids))+" solids" print(s,end="") objects[pid] = obj elif (preferences['MERGE_MODE_ARCH'] == 1 and archobj) or (preferences['MERGE_MODE_STRUCT'] == 0 and not archobj): # non-parametric Arch objects (just Arch components with a shape) if ptype in ["IfcSite","IfcBuilding","IfcBuildingStorey"]: for freecadtype,ifctypes in typesmap.items(): if ptype in ifctypes: obj = getattr(Arch,"make"+freecadtype)(baseobj=baseobj,name=name) if preferences['DEBUG']: print(": "+obj.Label+" ",end="") if ptype == "IfcBuildingStorey": if product.Elevation: obj.Placement.Base.z = product.Elevation * ifcscale elif baseobj: obj = Arch.makeComponent(baseobj,name=name,delete=True) obj.Label = name if preferences['DEBUG']: print(": "+obj.Label+" ",end="") if hasattr(obj,"Description") and hasattr(product,"Description"): if product.Description: obj.Description = product.Description try: if hasattr(obj,"IfcType"): obj.IfcType = ''.join(map(lambda x: x if x.islower() else " "+x, ptype[3:]))[1:] except Exception: print("Unable to give IFC type ",ptype," to object ",obj.Label) if hasattr(obj,"IfcData"): a = obj.IfcData a["IfcUID"] = str(guid) obj.IfcData = a elif pid in additions: # no baseobj but in additions, thus we make a BuildingPart container obj = getattr(Arch,"makeBuildingPart")(name=name) if preferences['DEBUG']: print(": "+obj.Label+" ",end="") else: if preferences['DEBUG']: print(": skipped.") continue if obj and hasattr(obj, "GlobalId"): obj.GlobalId = guid elif (preferences['MERGE_MODE_ARCH'] == 2 and archobj) or (preferences['MERGE_MODE_STRUCT'] == 1 and not archobj): # Part shapes if ptype in ["IfcSite","IfcBuilding","IfcBuildingStorey"]: for freecadtype,ifctypes in typesmap.items(): if ptype in ifctypes: obj = getattr(Arch,"make"+freecadtype)(baseobj=baseobj,name=name) if preferences['DEBUG']: print(": "+obj.Label+" ",end="") if ptype == "IfcBuildingStorey": if product.Elevation: obj.Placement.Base.z = product.Elevation * ifcscale elif baseobj: obj = doc.addObject("Part::Feature",name) obj.Shape = shape elif pid in additions: # no baseobj but in additions, thus we make a BuildingPart container obj = getattr(Arch,"makeBuildingPart")(name=name) if preferences['DEBUG']: print(": "+obj.Label+" ",end="") else: if preferences['DEBUG']: print(": skipped.") continue if preferences['DEBUG']: print("") # newline for debug prints, print for a new object should be on a new line if obj: obj.Label = name objects[pid] = obj # handle properties if psets: if preferences['IMPORT_PROPERTIES'] and hasattr(obj,"IfcProperties"): # treat as spreadsheet (pref option) if isinstance(obj.IfcProperties,dict): # fix property type if needed obj.removeProperty("IfcProperties") obj.addProperty("App::PropertyLink","IfcProperties","Component","Stores IFC properties as a spreadsheet") ifc_spreadsheet = Arch.makeIfcSpreadsheet() n = 2 for c in psets.keys(): o = ifcfile[c] if preferences['DEBUG']: print("propertyset Name",o.Name,type(o.Name)) catname = o.Name for p in psets[c]: l = ifcfile[p] lname = l.Name if l.is_a("IfcPropertySingleValue"): if preferences['DEBUG']: print("property name",l.Name,type(l.Name)) if six.PY2: catname = catname.encode("utf8") lname = lname.encode("utf8") ifc_spreadsheet.set(str('A'+str(n)), catname) ifc_spreadsheet.set(str('B'+str(n)), lname) if l.NominalValue: if preferences['DEBUG']: print("property NominalValue",l.NominalValue.is_a(),type(l.NominalValue.is_a())) print("property NominalValue.wrappedValue",l.NominalValue.wrappedValue,type(l.NominalValue.wrappedValue)) # print("l.NominalValue.Unit",l.NominalValue.Unit,type(l.NominalValue.Unit)) ifc_spreadsheet.set(str('C'+str(n)), l.NominalValue.is_a()) if l.NominalValue.is_a() in ['IfcLabel','IfcText','IfcIdentifier','IfcDescriptiveMeasure']: if six.PY2: ifc_spreadsheet.set(str('D'+str(n)), "'" + str(l.NominalValue.wrappedValue.encode("utf8"))) else: ifc_spreadsheet.set(str('D'+str(n)), "'" + str(l.NominalValue.wrappedValue)) else: ifc_spreadsheet.set(str('D'+str(n)), str(l.NominalValue.wrappedValue)) if hasattr(l.NominalValue,'Unit'): ifc_spreadsheet.set(str('E'+str(n)), str(l.NominalValue.Unit)) n += 1 obj.IfcProperties = ifc_spreadsheet elif hasattr(obj,"IfcProperties") and isinstance(obj.IfcProperties,dict): # 0.18 behaviour: properties are saved as pset;;type;;value in IfcProperties d = obj.IfcProperties obj.IfcProperties = importIFCHelper.getIfcProperties(ifcfile, pid, psets, d) elif hasattr(obj,"IfcData"): # 0.17: properties are saved as type(value) in IfcData a = obj.IfcData for c in psets.keys(): for p in psets[c]: l = ifcfile[p] if l.is_a("IfcPropertySingleValue"): a[l.Name.encode("utf8")] = str(l.NominalValue) # no py3 support here obj.IfcData = a # color if (pid in colors) and colors[pid]: colordict[obj.Name] = colors[pid] if FreeCAD.GuiUp: # if preferences['DEBUG']: # print(" setting color: ",int(colors[pid][0]*255),"/",int(colors[pid][1]*255),"/",int(colors[pid][2]*255)) if hasattr(obj.ViewObject,"ShapeColor"): obj.ViewObject.ShapeColor = tuple(colors[pid][0:3]) if hasattr(obj.ViewObject,"Transparency"): obj.ViewObject.Transparency = colors[pid][3] # if preferences['DEBUG'] is on, recompute after each shape if preferences['DEBUG']: doc.recompute() # attached 2D elements if product.Representation: for r in product.Representation.Representations: if r.RepresentationIdentifier == "FootPrint": annotations.append(product) break # additional properties for specific types if product.is_a("IfcSite"): if product.RefElevation: obj.Elevation = product.RefElevation * ifcscale if product.RefLatitude: obj.Latitude = importIFCHelper.dms2dd(*product.RefLatitude) if product.RefLongitude: obj.Longitude = importIFCHelper.dms2dd(*product.RefLongitude) if product.SiteAddress: if product.SiteAddress.AddressLines: obj.Address = product.SiteAddress.AddressLines[0] if product.SiteAddress.Town: obj.City = product.SiteAddress.Town if product.SiteAddress.Region: obj.Region = product.SiteAddress.Region if product.SiteAddress.Country: obj.Country = product.SiteAddress.Country if product.SiteAddress.PostalCode: obj.PostalCode = product.SiteAddress.PostalCode project = product.Decomposes[0].RelatingObject modelRC = next((rc for rc in project.RepresentationContexts if rc.ContextType == "Model"), None) if modelRC and modelRC.TrueNorth: # If the y-part of TrueNorth is 0, then the x-part should be checked. # Declination would be -90° if x >0 and +90° if x < 0 # Only if x==0 then we can not determine TrueNorth. # But that would actually be an invalid IFC file, because the magnitude # of the (twodimensional) direction vector for TrueNorth shall be greater than zero. (x, y) = modelRC.TrueNorth.DirectionRatios[:2] obj.Declination = ((math.degrees(math.atan2(y,x))-90+180) % 360)-180 if (FreeCAD.GuiUp): obj.ViewObject.CompassRotation.Value = obj.Declination try: progressbar.next(True) except(RuntimeError): print("Aborted.") progressbar.stop() doc.recompute() return progressbar.stop() doc.recompute() if preferences['MERGE_MODE_STRUCT'] == 2: if preferences['DEBUG']: print("Joining Structural shapes...",end="") for host,children in groups.items(): # Structural if ifcfile[host].is_a("IfcStructuralAnalysisModel"): compound = [] for c in children: if c in structshapes.keys(): compound.append(structshapes[c]) del structshapes[c] if compound: name = ifcfile[host].Name or "AnalysisModel" if preferences['PREFIX_NUMBERS']: name = "ID" + str(host) + " " + name obj = doc.addObject("Part::Feature",name) obj.Label = name obj.Shape = Part.makeCompound(compound) if structshapes: # remaining Structural shapes obj = doc.addObject("Part::Feature","UnclaimedStruct") obj.Shape = Part.makeCompound(structshapes.values()) if preferences['DEBUG']: print("done") else: if preferences['DEBUG']: print("Processing Struct relationships...",end="") # groups for host,children in groups.items(): if ifcfile[host].is_a("IfcStructuralAnalysisModel"): # print(host, ' --> ', children) obj = doc.addObject("App::DocumentObjectGroup","AnalysisModel") objects[host] = obj if host in objects.keys(): cobs = [] childs_to_delete = [] for child in children: if child in objects.keys(): cobs.append(objects[child]) childs_to_delete.append(child) for c in childs_to_delete: children.remove(c) # to not process the child again in remaining groups if cobs: if preferences['DEBUG']: print("adding ",len(cobs), " object(s) to ", objects[host].Label) Arch.addComponents(cobs,objects[host]) if preferences['DEBUG']: doc.recompute() if preferences['DEBUG']: print("done") if preferences['MERGE_MODE_ARCH'] > 2: # if ArchObj is compound or ArchObj not imported doc.recompute() # cleaning bad shapes for obj in objects.values(): if obj.isDerivedFrom("Part::Feature"): if obj.Shape.isNull(): Arch.rebuildArchShape(obj) # processing remaining (normal) groups swallowed = [] remaining = {} for host,children in groups.items(): if ifcfile[host].is_a("IfcGroup"): if ifcfile[host].Name: grp_name = ifcfile[host].Name else: if preferences['DEBUG']: print("no group name specified for entity: #", ifcfile[host].id(), ", entity type is used!") grp_name = ifcfile[host].is_a() + "_" + str(ifcfile[host].id()) if six.PY2: grp_name = grp_name.encode("utf8") grp = doc.addObject("App::DocumentObjectGroup",grp_name) grp.Label = grp_name objects[host] = grp for child in children: if child in objects.keys(): grp.addObject(objects[child]) swallowed.append(child) else: remaining[child] = grp if preferences['MERGE_MODE_ARCH'] == 3: # One compound per storey if preferences['DEBUG']: print("Joining Arch shapes...",end="") for host,children in additions.items(): # Arch if ifcfile[host].is_a("IfcBuildingStorey"): compound = [] for c in children: if c in shapes.keys(): compound.append(shapes[c]) del shapes[c] if c in additions.keys(): for c2 in additions[c]: if c2 in shapes.keys(): compound.append(shapes[c2]) del shapes[c2] if compound: name = ifcfile[host].Name or "Floor" if preferences['PREFIX_NUMBERS']: name = "ID" + str(host) + " " + name obj = doc.addObject("Part::Feature",name) obj.Label = name obj.Shape = Part.makeCompound(compound) if shapes: # remaining Arch shapes obj = doc.addObject("Part::Feature","UnclaimedArch") obj.Shape = Part.makeCompound(shapes.values()) if preferences['DEBUG']: print("done") else: if preferences['DEBUG']: print("Processing Arch relationships...",end="") first = True # subtractions if preferences['SEPARATE_OPENINGS']: for subtraction in subtractions: if (subtraction[0] in objects.keys()) and (subtraction[1] in objects.keys()): if preferences['DEBUG'] and first: print("") first = False if preferences['DEBUG']: print(" subtracting",objects[subtraction[0]].Label, "from", objects[subtraction[1]].Label) Arch.removeComponents(objects[subtraction[0]],objects[subtraction[1]]) if preferences['DEBUG']: doc.recompute() # additions for host,children in additions.items(): if host not in objects.keys(): # print(host, 'not used') # print(ifcfile[host]) continue cobs = [] for child in children: if child in objects.keys() \ and child not in swallowed: # don't add objects already in groups cobs.append(objects[child]) if not cobs: continue if preferences['DEBUG'] and first: print("") first = False if ( preferences['DEBUG'] and (len(cobs) > 10) and (not(Draft.getType(objects[host]) in ["Site","Building","Floor","BuildingPart","Project"])) ): # avoid huge fusions print("more than 10 shapes to add: skipping.") else: if preferences['DEBUG']: print(" adding",len(cobs), "object(s) to", objects[host].Label) Arch.addComponents(cobs,objects[host]) if preferences['DEBUG']: doc.recompute() if preferences['DEBUG'] and first: print("done.") doc.recompute() # cleaning bad shapes for obj in objects.values(): if obj.isDerivedFrom("Part::Feature"): if obj.Shape.isNull() and not(Draft.getType(obj) in ["Site","Project"]): Arch.rebuildArchShape(obj) doc.recompute() # 2D elements if preferences['DEBUG'] and annotations: print("Processing",len(annotations),"2D objects...") prodcount = count count = 0 for annotation in annotations: anno = None aid = annotation.id() count += 1 if preferences['DEBUG']: print(count,"/",len(annotations),"object #"+str(aid),":",annotation.is_a(),end="") if aid in skip: continue # user given id skip list if annotation.is_a() in preferences['SKIP']: continue # preferences-set type skip list if annotation.is_a("IfcGrid"): axes = [] uvwaxes = () if annotation.UAxes: uvwaxes = annotation.UAxes if annotation.VAxes: uvwaxes = uvwaxes + annotation.VAxes if annotation.WAxes: uvwaxes = uvwaxes + annotation.WAxes for axis in uvwaxes: if axis.AxisCurve: sh = importIFCHelper.get2DShape(axis.AxisCurve,ifcscale) if sh and (len(sh[0].Vertexes) == 2): # currently only straight axes are supported sh = sh[0] l = sh.Length pl = FreeCAD.Placement() pl.Base = sh.Vertexes[0].Point pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0,1,0),sh.Vertexes[-1].Point.sub(sh.Vertexes[0].Point)) o = Arch.makeAxis(1,l) o.Length = l o.Placement = pl o.CustomNumber = axis.AxisTag axes.append(o) if axes: name = "Grid" grid_placement = None if annotation.Name: name = annotation.Name if six.PY2: name = name.encode("utf8") if annotation.ObjectPlacement: # https://forum.freecadweb.org/viewtopic.php?f=39&t=40027 grid_placement = importIFCHelper.getPlacement( annotation.ObjectPlacement, scaling=1 ) if preferences['PREFIX_NUMBERS']: name = "ID" + str(aid) + " " + name anno = Arch.makeAxisSystem(axes,name) if grid_placement: anno.Placement = grid_placement print(" axis") else: name = "Annotation" if annotation.Name: name = annotation.Name if six.PY2: name = name.encode("utf8") if "annotation" not in name.lower(): name = "Annotation " + name if preferences['PREFIX_NUMBERS']: name = "ID" + str(aid) + " " + name shapes2d = [] for rep in annotation.Representation.Representations: if rep.RepresentationIdentifier in ["Annotation","FootPrint","Axis"]: sh = importIFCHelper.get2DShape(rep,ifcscale) if sh in doc.Objects: # dirty hack: get2DShape might return an object directly if non-shape based (texts for ex) anno = sh else: shapes2d.extend(sh) if shapes2d: sh = Part.makeCompound(shapes2d) if preferences['DEBUG']: print(" shape") anno = doc.addObject("Part::Feature",name) anno.Shape = sh p = importIFCHelper.getPlacement(annotation.ObjectPlacement,ifcscale) if p: # and annotation.is_a("IfcAnnotation"): anno.Placement = p else: if preferences['DEBUG']: print(" no shape") # placing in container if needed if anno: if aid in remaining.keys(): remaining[aid].addObject(anno) else: for host,children in additions.items(): if (aid in children) and (host in objects.keys()): Arch.addComponents(anno,objects[host]) doc.recompute() # Materials if preferences['DEBUG'] and materials: print("Creating materials...",end="") # print("\n") # print("colors:",colors) # print("mattable:",mattable) # print("materials:",materials) added_mats = [] for material in materials: # print(material.id()) mdict = {} # on ifc import only the "Description" and "DiffuseColor" (if it was read) of the material dictionary will be initialized # on editing material in Arch Gui a lot more keys of the material dictionary are initialized even with empty values # TODO: there should be a generic material obj init method which will be used by Arch Gui, import IFC, FEM, etc # get the material name name = "Material" if material.Name: name = material.Name if six.PY2: name = name.encode("utf8") # mdict["Name"] = name on duplicate material names in IFC this could result in crash # https://forum.freecadweb.org/viewtopic.php?f=23&t=63260 # thus use "Description" mdict["Description"] = name # get material color # the "DiffuseColor" of a material should never be "None" # values in colors are None if something went wrong # thus the "DiffuseColor" will only be set if the color is not None mat_color = None if material.id() in colors and colors[material.id()] is not None: mat_color = str(colors[material.id()]) else: for o,m in mattable.items(): if m == material.id(): if o in colors and colors[o] is not None: mat_color = str(colors[o]) if mat_color is not None: mdict["DiffuseColor"] = mat_color else: if preferences['DEBUG']: print("/n no color for material: {}, ".format(str(material.id)),end="") # merge materials with same name and color if setting in prefs is True add_material = True if preferences["MERGE_MATERIALS"]: for added_mat in added_mats: if ( "Description" in added_mat.Material and "DiffuseColor" in added_mat.Material and "DiffuseColor" in mdict # Description has been set thus it is in mdict and added_mat.Material["Description"] == mdict["Description"] and added_mat.Material["DiffuseColor"] == mdict["DiffuseColor"] ): matobj = added_mat add_material = False break # add a new material object if add_material is True: matobj = Arch.makeMaterial(name=name) matobj.Material = mdict added_mats.append(matobj) # fill material attribute of the objects for o,m in mattable.items(): if m == material.id(): if o in objects: if hasattr(objects[o],"Material"): objects[o].Material = matobj if FreeCAD.GuiUp: # the reason behind ... # there are files around in which the material color is different from the shape color # all viewers use the shape color whereas in FreeCAD the shape color will be # overwritten by the material color (if there is a material with a color). # In such a case FreeCAD shows a different color than all common ifc viewers # https://forum.freecadweb.org/viewtopic.php?f=39&t=38440 col = objects[o].ViewObject.ShapeColor[:3] dig = 5 ma_color = sh_color = round(col[0], dig), round(col[1], dig), round(col[2], dig) if "DiffuseColor" in objects[o].Material.Material: string_color = objects[o].Material.Material["DiffuseColor"] col = tuple([float(f) for f in string_color.strip("()").split(",")]) ma_color = round(col[0], dig), round(col[1], dig), round(col[2], dig) if ma_color != sh_color: print("\nobject color != material color for object: ", o) print(" material color is used (most software uses shape color)") print(" obj: ", o, "label: ", objects[o].Label, " col: ", sh_color) print(" mat: ", m, "label: ", matobj.Label, " col: ", ma_color) # print(" ", ifcfile[o]) # print(" ", ifcfile[m]) # print(" colors:") # print(" ", o, ": ", colors[o]) # print(" ", m, ": ", colors[m]) if preferences['DEBUG'] and materials: print("done") # Grouping everything if required # has to be before the Layer # if REPLACE_PROJECT and only one storey and one building both are omitted # the pure objects do not belong to any container, they will be added here # if after Layer they are linked by Layer and will not be added here if preferences["REPLACE_PROJECT"] and filename: rootgroup = doc.addObject("App::DocumentObjectGroup","Group") rootgroup.Label = os.path.basename(filename) # print(objects) for key,obj in objects.items(): # only add top-level objects if not obj.InList: rootgroup.addObject(obj) # Layers if preferences['DEBUG'] and layers: print("Creating layers...", end="") # print(layers) for layer_name, layer_objects in layers.items(): if preferences["IMPORT_LAYER"] is False: continue # the method make_layer does some nasty debug prints lay = Draft.make_layer(layer_name) # ShapeColor and LineColor are not set, thus some some default values are used # do not override the imported ShapeColor and LineColor with default layer values if FreeCAD.GuiUp: lay.ViewObject.OverrideLineColorChildren = False lay.ViewObject.OverrideShapeColorChildren = False lay_grp = [] for lobj_id in layer_objects: if lobj_id in objects: lay_grp.append(objects[lobj_id]) lay.Group = lay_grp doc.recompute() if preferences['DEBUG'] and layers: print("done") # restore links from full parametric definitions for p in parametrics: l = doc.getObject(p[2]) if l: setattr(p[0],p[1],l) # Save colordict in non-GUI mode if colordict and not FreeCAD.GuiUp: import json d = doc.Meta d["colordict"] = json.dumps(colordict) doc.Meta = d doc.recompute() if FreeCAD.GuiUp and ZOOMOUT: Gui.SendMsgToActiveView("ViewFit") endtime = time.time()-starttime if filesize: _msg("Finished importing {0} MB " "in {1} seconds, or {2} s/MB".format(round(filesize, 1), int(endtime), int(endtime/filesize))) else: _msg("Finished importing in {} seconds".format(int(endtime))) return doc