BIM: Added classifications support to nativeifc (#18444)

* BIM: Added classifications support to nativeifc

* BIM: Fixed lint warnings
This commit is contained in:
Yorik van Havre
2025-01-07 10:35:37 +01:00
committed by GitHub
parent 6deb424539
commit b5e2e8c007
11 changed files with 279 additions and 92 deletions

View File

@@ -528,6 +528,8 @@ class ViewProviderCurtainWall(ArchComponent.ViewProviderComponent):
if not obj.Shape or not obj.Shape.Solids:
return
if not obj.ViewObject:
return
basecolor = obj.ViewObject.ShapeColor
basetransparency = obj.ViewObject.Transparency/100.0
panelcolor = ArchCommands.getDefaultColor("WindowGlass")

View File

@@ -193,6 +193,7 @@ SET(nativeifc_SRCS
nativeifc/ifc_openshell.py
nativeifc/ifc_types.py
nativeifc/ifc_export.py
nativeifc/ifc_classification.py
)
SOURCE_GROUP("" FILES ${Arch_SRCS})

View File

@@ -58,6 +58,7 @@ class BIM_Classification:
if not hasattr(self, "Classes"):
self.Classes = {}
self.isEditing = None
current = None
# load the form and set the tree model up
self.form = FreeCADGui.PySideUic.loadUi(":/ui/dialogClassification.ui")
@@ -86,13 +87,22 @@ class BIM_Classification:
# hide materials list if we are editing a particular object
if len(FreeCADGui.Selection.getSelection()) == 1:
self.isEditing = FreeCADGui.Selection.getSelection()[0]
if hasattr(self.isEditing, "StandardCode"):
pl = self.isEditing.PropertiesList
if ("StandardCode" in pl) or ("IfcClass" in pl):
self.form.groupMaterials.hide()
self.form.buttonApply.hide()
self.form.buttonRename.hide()
self.form.setWindowTitle(
translate("BIM", "Editing") + " " + self.isEditing.Label
)
if "IfcClass" in pl:
# load existing class if needed
from nativeifc import ifc_classification
ifc_classification.show_classification(self.isEditing)
if "StandardCode" in pl:
current = self.isEditing.StandardCode
elif "Classification" in self.isEditing.PropertiesList:
current = self.isEditing.Classification
# fill materials list
self.objectslist = {}
@@ -105,6 +115,9 @@ class BIM_Classification:
else:
self.objectslist[obj.Name] = obj.StandardCode
self.labellist[obj.Name] = obj.Label
elif "Classification" in obj.PropertiesList:
self.objectslist[obj.Name] = obj.Classification
self.labellist[obj.Name] = obj.Label
# fill objects list
if not self.isEditing:
@@ -129,37 +142,15 @@ class BIM_Classification:
)
# connect signals
QtCore.QObject.connect(
self.form.comboSystem,
QtCore.SIGNAL("currentIndexChanged(int)"),
self.updateClasses,
)
QtCore.QObject.connect(
self.form.buttonApply, QtCore.SIGNAL("clicked()"), self.apply
)
QtCore.QObject.connect(
self.form.buttonRename, QtCore.SIGNAL("clicked()"), self.rename
)
QtCore.QObject.connect(
self.form.search, QtCore.SIGNAL("textEdited(QString)"), self.updateClasses
)
QtCore.QObject.connect(
self.form.buttonBox, QtCore.SIGNAL("accepted()"), self.accept
)
QtCore.QObject.connect(
self.form.groupMode,
QtCore.SIGNAL("currentIndexChanged(int)"),
self.updateObjects,
)
QtCore.QObject.connect(
self.form.treeClass,
QtCore.SIGNAL("itemDoubleClicked(QTreeWidgetItem*,int)"),
self.apply,
)
QtCore.QObject.connect(self.form.search, QtCore.SIGNAL("up()"), self.onUpArrow)
QtCore.QObject.connect(
self.form.search, QtCore.SIGNAL("down()"), self.onDownArrow
)
self.form.comboSystem.currentIndexChanged.connect(self.updateClasses)
self.form.buttonApply.clicked.connect(self.apply)
self.form.buttonRename.clicked.connect(self.rename)
self.form.search.textEdited.connect(self.updateClasses)
self.form.buttonBox.accepted.connect(self.accept)
self.form.groupMode.currentIndexChanged.connect(self.updateObjects)
self.form.treeClass.itemDoubleClicked.connect(self.apply)
self.form.search.up.connect(self.onUpArrow)
self.form.search.down.connect(self.onDownArrow)
# center the dialog over FreeCAD window
mw = FreeCADGui.getMainWindow()
@@ -170,6 +161,21 @@ class BIM_Classification:
)
self.updateClasses()
# select current classification
if current:
system, classification = current.split(" ", 1)
print("searching for",classification)
if system in self.Classes:
self.form.comboSystem.setCurrentText(system)
res = self.form.treeClass.findItems(
classification,
QtCore.Qt.MatchExactly|QtCore.Qt.MatchRecursive,
0
)
if res:
self.form.treeClass.setCurrentItem(res[0])
self.form.show()
self.form.search.setFocus()
@@ -579,13 +585,24 @@ class BIM_Classification:
if item.toolTip(0):
obj = FreeCAD.ActiveDocument.getObject(item.toolTip(0))
if obj:
if code != obj.StandardCode:
if not changed:
FreeCAD.ActiveDocument.openTransaction(
"Change standard codes"
)
changed = True
obj.StandardCode = code
if hasattr(obj, "StandardCode"):
if code != obj.StandardCode:
if not changed:
FreeCAD.ActiveDocument.openTransaction(
"Change standard codes"
)
changed = True
obj.StandardCode = code
elif hasattr(obj, "IfcClass"):
if not "Classification" in obj.PropertiesList:
obj.addProperty("App::PropertyString", "Classification", "IFC")
if code != obj.Classification:
if not changed:
FreeCAD.ActiveDocument.openTransaction(
"Change standard codes"
)
changed = True
obj.Classification = code
if label != obj.Label:
if not changed:
FreeCAD.ActiveDocument.openTransaction(
@@ -598,11 +615,17 @@ class BIM_Classification:
FreeCAD.ActiveDocument.recompute()
else:
code = self.form.treeClass.selectedItems()[0].text(0)
if "StandardCode" in self.isEditing.PropertiesList:
pl = self.isEditing.PropertiesList
if ("StandardCode" in pl) or ("IfcClass" in pl):
FreeCAD.ActiveDocument.openTransaction("Change standard codes")
if self.form.checkPrefix.isChecked():
code = self.form.comboSystem.currentText() + " " + code
self.isEditing.StandardCode = code
if "StandardCode" in pl:
self.isEditing.StandardCode = code
else:
if not "Classification" in self.isEditing.PropertiesList:
self.isEditing.addProperty("App::PropertyString", "Classification", "IFC")
self.isEditing.Classification = code
if hasattr(self.isEditing.ViewObject, "Proxy") and hasattr(
self.isEditing.ViewObject.Proxy, "setTaskValue"
):

View File

@@ -36,14 +36,17 @@ if FreeCAD.GuiUp:
class MatLineEdit(QtGui.QLineEdit):
"custom QLineEdit widget that has the power to catch up/down arrow keypress"
up = QtCore.Signal()
down = QtCore.Signal()
def __init__(self, parent=None):
QtGui.QLineEdit.__init__(self, parent)
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Up:
self.emit(QtCore.SIGNAL("up()"))
self.up.emit()
elif event.key() == QtCore.Qt.Key_Down:
self.emit(QtCore.SIGNAL("down()"))
self.down.emit()
else:
QtGui.QLineEdit.keyPressEvent(self, event)
@@ -435,8 +438,8 @@ class BIM_Material:
FreeCAD.ActiveDocument.openTransaction("Change material")
for obj in self.dlg.objects:
if hasattr(obj, "StepId"):
from nativeifc import ifc_tools
ifc_tools.set_material(mat, obj)
from nativeifc import ifc_materials
ifc_materials.set_material(mat, obj)
else:
obj.Material = mat
FreeCAD.ActiveDocument.commitTransaction()

View File

@@ -48,8 +48,7 @@ from importers.importIFCHelper import dd2dms
from draftutils import params
from draftutils.messages import _msg, _err
if FreeCAD.GuiUp:
import FreeCADGui
import FreeCADGui
__title__ = "FreeCAD IFC export"
__author__ = ("Yorik van Havre", "Jonathan Wiedemann", "Bernd Hahnebach")
@@ -339,6 +338,8 @@ def export(exportList, filename, colors=None, preferences=None):
shapedefs = {} # { ShapeDefString:[shapes],... }
spatialelements = {} # {Name:IfcEntity, ... }
uids = [] # store used UIDs to avoid reuse (some FreeCAD objects might have same IFC UID, ex. copy/pasted objects
classifications = {} # {Name:IfcEntity, ... }
curvestyles = {}
# build clones table
@@ -934,6 +935,33 @@ def export(exportList, filename, colors=None, preferences=None):
pset
)
# Classifications
classification = getattr(obj, "StandardCode", "")
if classification:
name, code = classification.split(" ", 1)
if name in classifications:
system = classifications[name]
else:
system = ifcfile.createIfcClassification(None, None, None, name)
classifications[name] = system
for ref in getattr(system, "HasReferences", []):
if code.startswith(ref.Name):
break
else:
ref = ifcfile.createIfcClassificationReference(None, code, None, system)
if getattr(ref, "ClassificationRefForObjects", None):
rel = ref.ClassificationRefForObjects[0]
rel.RelatedObjects = rel.RelatedObjects + [product]
else:
rel = ifcfile.createIfcRelAssociatesClassification(
ifcopenshell.guid.new(),
history,'FreeCADClassificationRel',
None,
[product],
ref
)
count += 1
# relate structural analysis objects to the struct model
@@ -2471,7 +2499,7 @@ def writeJson(filename,ifcfile):
def create_annotation(anno, ifcfile, context, history, preferences):
"""Creates an annotation object"""
# uses global ifcbin, curvestyles
global curvestyles, ifcbin
objectType = None
ovc = None
zvc = None
@@ -2479,6 +2507,7 @@ def create_annotation(anno, ifcfile, context, history, preferences):
reps = []
repid = "Annotation"
reptype = "Annotation2D"
description = getattr(anno, "Description", None)
if anno.isDerivedFrom("Part::Feature"):
if Draft.getType(anno) == "Hatch":
objectType = "HATCH"

View File

@@ -0,0 +1,104 @@
# ***************************************************************************
# * *
# * Copyright (c) 2024 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 General Public License (GPL) *
# * as published by the Free Software Foundation; either version 3 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 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 *
# * *
# ***************************************************************************
from nativeifc import ifc_tools # lazy import
def edit_classification(obj):
"""Edits the classification of this object"""
element = ifc_tools.get_ifc_element(obj)
ifcfile = ifc_tools.get_ifcfile(obj)
if not element or not ifcfile:
return
# TODO: remove previous reference?
#ifc_tools.api_run("classification.remove_reference",
# ifcfile, reference=ref, products=[obj])
classifications = ifcfile.by_type("IfcClassification")
classification = getattr(obj, "Classification", "")
if classification:
cname, code = classification.split(" ", 1)
cnames = [c.Name for c in classifications]
if cname in cnames:
system = classifications[cnames.index(cname)]
else:
system = ifc_tools.api_run("classification.add_classification", ifcfile,
classification=cname)
for ref in getattr(system, "HasReferences", []):
rname = ref.Name or ref.Identification
if code == rname:
return
elif code.startswith(rname):
if getattr(ref, "ClassificationRefForObjects", None):
rel = ref.ClassificationRefForObjects[0]
if not element in rel.RelatedObjects:
ifc_tools.edit_attribute(rel, "RelatedObjects",
rel.RelatedObjects + [element]
)
else:
# we have a reference, but no classForObjects
# this is weird and shouldn't exist...
rel = ifcfile.createIfcRelAssociatesClassification(
ifc_tools.ifcopenshell.guid.new(),
history,'FreeCADClassificationRel',
None,
[element],
ref
)
else:
ifc_tools.api_run("classification.add_reference", ifcfile,
products = [element],
classification = system,
identification = code
)
else:
# classification property is empty
for rel in getattr(element, "HasAssociations", []):
if rel.is_a("IfcRelAssociatesClassification"):
# removing existing classification if only user
if len(rel.RelatedObjects) == 1 and rel.RelatedObjects[0] == element:
ifc_tools.api_run("classification.remove_reference",
ifcfile,
reference=rel.RelatingClassification,
products=[element]
)
# TODO: Remove IfcClassification too?
def show_classification(obj):
"""Loads the classification of this object"""
element = ifc_tools.get_ifc_element(obj)
ifcfile = ifc_tools.get_ifcfile(obj)
if not element or not ifcfile:
return
for system in ifcfile.by_type("IfcClassification"):
for ref in getattr(system, "HasReferences", []):
for rel in ref.ClassificationRefForObjects:
if element in rel.RelatedObjects:
if not "Classification" in obj.PropertiesList:
obj.addProperty("App::PropertyString", "Classification", "IFC")
sname = system.Name
cname = ref.Name or ref.Identification
obj.Classification = sname + " " + cname
break

View File

@@ -23,6 +23,7 @@
"""This module contains IFC object definitions"""
import FreeCAD
import FreeCADGui
translate = FreeCAD.Qt.translate
# the property groups below should not be treated as psets
@@ -59,7 +60,9 @@ class ifc_object:
elif prop == "Schema":
self.edit_schema(obj, obj.Schema)
elif prop == "Type":
self.edit_type(obj)
self.Classification(obj)
elif prop == "Classification":
self.edit_classification(obj)
elif prop == "Group":
self.edit_group(obj)
elif hasattr(obj, prop) and obj.getGroupOfProperty(prop) == "IFC":
@@ -111,11 +114,7 @@ class ifc_object:
def fit_all(self):
"""Fits the view"""
import FreeCAD
if FreeCAD.GuiUp:
import FreeCADGui
FreeCADGui.SendMsgToActiveView("ViewFit")
def rebuild_classlist(self, obj, setprops=False):
@@ -322,38 +321,10 @@ class ifc_object:
def edit_type(self, obj):
"""Edits the type of this object"""
from nativeifc import ifc_tools # lazy import
from nativeifc import ifc_types
from nativeifc import ifc_types # lazy import
ifc_types.edit_type(obj)
element = ifc_tools.get_ifc_element(obj)
ifcfile = ifc_tools.get_ifcfile(obj)
if not element or not ifcfile:
return
typerel = getattr(element, "IsTypedBy", None)
if obj.Type:
# verify the type is compatible -ex IFcWall in IfcWallType
if obj.Type.Class != element.is_a() + "Type":
t = translate("BIM","Error: Incompatible type")
FreeCAD.Console.PrintError(obj.Label+": "+t+": "+obj.Type.Class+"\n")
obj.Type = None
return
# change type
new_type = ifc_tools.get_ifc_element(obj.Type)
if not new_type:
return
for rel in typerel:
if rel.RelatingType == new_type:
return
# assign the new type
ifc_tools.api_run("type.assign_type",
ifcfile,
related_objects=[element],
relating_type=new_type
)
elif typerel:
# TODO remove type?
# Not doing anything right now because an unset Type property could screw the ifc file
pass
def edit_quantity(self, obj, prop):
@@ -403,6 +374,14 @@ class ifc_object:
return [], None
def edit_classification(self, obj):
"""Edits the classification of this object"""
from nativeifc import ifc_classification # lazy loading
ifc_classification.edit_classification(obj)
class document_object:
"""Holder for the document's IFC objects"""

View File

@@ -248,6 +248,10 @@ def on_activate():
from PySide import QtGui # lazy import
# always reset the menu to normal first
set_menu(False)
if FreeCADGui.activeWorkbench().name() != "BIMWorkbench":
return
doc = FreeCAD.ActiveDocument
if doc and "IfcFilePath" in doc.PropertiesList:
checked = True

View File

@@ -185,9 +185,11 @@ def create_ifcfile():
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)
org = param.GetString("prefCompany", "")
if org:
organisation = api_run("owner.add_organisation", ifcfile, name=org)
if user and org:
@@ -1633,11 +1635,14 @@ def remove_tree(objs):
def recompute(children):
"""Temporary function to recompute objects. Some objects don't get their
shape correctly at creation"""
import time
stime = time.time()
#import time
#stime = time.time()
doc = None
for c in children:
c.touch()
if not FreeCAD.ActiveDocument.Recomputing:
FreeCAD.ActiveDocument.recompute()
endtime = "%02d:%02d" % (divmod(round(time.time() - stime, 1), 60))
print("DEBUG: Extra recomputing of",len(children),"objects took",endtime)
if c:
c.touch()
doc = c.Document
if doc:
doc.recompute()
#endtime = "%02d:%02d" % (divmod(round(time.time() - stime, 1), 60))
#print("DEBUG: Extra recomputing of",len(children),"objects took",endtime)

View File

@@ -91,3 +91,38 @@ def convert_to_type(obj, keep_object=False):
else:
ifc_tools.remove_ifc_element(obj, delete_obj=True)
ifc_tools.get_group(project, "IfcTypesGroup").addObject(type_obj)
def edit_type(obj):
"""Edits the type of this object"""
element = ifc_tools.get_ifc_element(obj)
ifcfile = ifc_tools.get_ifcfile(obj)
if not element or not ifcfile:
return
typerel = getattr(element, "IsTypedBy", None)
if obj.Type:
# verify the type is compatible -ex IFcWall in IfcWallType
if obj.Type.Class != element.is_a() + "Type":
t = translate("BIM","Error: Incompatible type")
FreeCAD.Console.PrintError(obj.Label+": "+t+": "+obj.Type.Class+"\n")
obj.Type = None
return
# change type
new_type = ifc_tools.get_ifc_element(obj.Type)
if not new_type:
return
for rel in typerel:
if rel.RelatingType == new_type:
return
# assign the new type
ifc_tools.api_run("type.assign_type",
ifcfile,
related_objects=[element],
relating_type=new_type
)
elif typerel:
# TODO remove type?
# Not doing anything right now because an unset Type property could screw the ifc file
pass

View File

@@ -380,6 +380,7 @@ class ifc_vp_object:
from nativeifc import ifc_materials # lazy import
from nativeifc import ifc_layers # lazy import
from nativeifc import ifc_types # lazy import
from nativeifc import ifc_classification # lazy import
# generic data loading
ifc_geometry.add_geom_properties(vobj.Object)
@@ -387,6 +388,7 @@ class ifc_vp_object:
ifc_materials.show_material(vobj.Object)
ifc_layers.add_layers(vobj.Object)
ifc_types.show_type(vobj.Object)
ifc_classification.show_classification(vobj.Object)
# expand children
if self.hasChildren(vobj.Object):