Files
create/src/Mod/Arch/importIFC.py
2022-09-15 15:40:33 +02:00

1317 lines
57 KiB
Python

# ***************************************************************************
# * Copyright (c) 2014 Yorik van Havre <yorik@uncreated.net> *
# * *
# * 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"
)
# backwards compatibility
getPreferences = importIFCHelper.getPreferences
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 = importIFCHelper.getPreferences()
if preferences["MULTICORE"] and not hasattr(srcfile, "by_guid"):
# override with BIM IFC importer if present
try:
import BimIfcImport
return BimIfcImport.insert(srcfile, docname, preferences)
except:
pass
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 (freecadtype in ["Rebar"]) and baseobj:
# TODO rebars don't keep link to their baee object - we can remove it
bn = baseobj.Name
doc.removeObject(bn)
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
anno = importIFCHelper.createAnnotation(annotation,doc,ifcscale,preferences)
# 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