Files
create/src/Mod/BIM/nativeifc/ifc_tools.py
tetektoza 2c82bfa150 BIM: Add an option to preload IFC types during document opening (#21450)
* BIM: Add an option to preload IFC types during document opening

Currently, IFC types are only possible to be loaded if user double
clicks an IFC object, and this has be done for optimization reasons.

But user can also want to preload IFC types, so this patch adds an
option to the dialog and Properties dialog to do just that.

* BIM: Remove cyclic import

---------

Co-authored-by: Yorik van Havre <yorik@uncreated.net>
2025-07-07 16:06:43 +02:00

1710 lines
64 KiB
Python

# SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# * *
# * Copyright (c) 2022 Yorik van Havre <yorik@uncreated.net> *
# * *
# * 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 *
# * <https://www.gnu.org/licenses/>. *
# * *
# ***************************************************************************
"""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)
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. + value[2]/3600. + value[3]/3600.e6
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.Text = [text.Literal]
obj.Placement = ifc_export.get_placement(ifcentity.ObjectPlacement, ifcfile)
obj.Length = axisdata[1]
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.e6))
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
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"
objecttype = None
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:
shapemode = getattr(parent, "ShapeMode", DEFAULT_SHAPEMODE)
newobj = create_object(product, obj.Document, ifcfile, shapemode, objecttype)
new = True
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()