BIM: Quantities support for nativeIFC objects (#18689)

* BIM: Quantities support for nativeIFC objects

* BIM: Added nativeIFC support for schedules
This commit is contained in:
Yorik van Havre
2025-01-06 17:55:50 +01:00
committed by GitHub
parent fccb75e364
commit f7a39fc313
19 changed files with 1407 additions and 80221 deletions

View File

@@ -25,13 +25,15 @@
"""This module contains FreeCAD commands for the BIM workbench"""
import os
import csv
import FreeCAD
import FreeCADGui
QT_TRANSLATE_NOOP = FreeCAD.Qt.QT_TRANSLATE_NOOP
translate = FreeCAD.Qt.translate
PARAMS = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/BIM")
qprops = [
QPROPS = [
"Length",
"Width",
"Height",
@@ -39,8 +41,8 @@ qprops = [
"HorizontalArea",
"VerticalArea",
"Volume",
] # quantities columns
trqprops = [
]
TR_QPROPS = [
translate("BIM", "Length"),
translate("BIM", "Width"),
translate("BIM", "Height"),
@@ -49,6 +51,15 @@ trqprops = [
translate("BIM", "Vertical Area"),
translate("BIM", "Volume"),
]
QTO_TYPES = {
"IfcQuantityArea": "App::PropertyArea",
"IfcQuantityCount": "App::PropertyInteger",
"IfcQuantityLength": "App::PropertyLength",
"IfcQuantityNumber": "App::PropertyInteger",
"IfcQuantityTime": "App::PropertyTime",
"IfcQuantityVolume": "App::PropertyVolume",
"IfcQuantityWeight": "App::PropertyWeight",
}
class BIM_IfcQuantities:
@@ -74,6 +85,7 @@ class BIM_IfcQuantities:
# build objects list
self.objectslist = {}
self.ifcqtolist = {}
for obj in FreeCAD.ActiveDocument.Objects:
role = self.getRole(obj)
if role:
@@ -95,17 +107,22 @@ class BIM_IfcQuantities:
# load the form and set the tree model up
self.form = FreeCADGui.PySideUic.loadUi(":/ui/dialogIfcQuantities.ui")
self.form.setWindowIcon(QtGui.QIcon(":/icons/BIM_IfcQuantities.svg"))
w = PARAMS.GetInt("BimIfcQuantitiesDialogWidth", 680)
h = PARAMS.GetInt("BimIfcQuantitiesDialogHeight", 512)
self.form.resize(w, h)
self.get_qtos()
# quantities tab
self.qmodel = QtGui.QStandardItemModel()
self.form.quantities.setModel(self.qmodel)
self.form.quantities.setUniformRowHeights(True)
self.form.quantities.setItemDelegate(QtGui.QStyledItemDelegate())
self.quantitiesDrawn = False
self.qmodel.dataChanged.connect(self.setChecked)
self.form.buttonBox.accepted.connect(self.accept)
self.form.quantities.clicked.connect(self.onClickTree)
self.form.onlyVisible.stateChanged.connect(self.update)
self.form.buttonRefresh.clicked.connect(self.update)
self.form.buttonApply.clicked.connect(self.add_qto)
# center the dialog over FreeCAD window
mw = FreeCADGui.getMainWindow()
@@ -132,137 +149,256 @@ class BIM_IfcQuantities:
def decamelize(self, s):
return "".join([" " + c if c.isupper() else c for c in s]).strip(" ")
def get_qtos(self):
"populates the qtos combo box"
def read_csv(csvfile):
result = {}
if os.path.exists(csvfile):
with open(csvfile, "r") as f:
reader = csv.reader(f, delimiter=";")
for row in reader:
result[row[0]] = row[1:]
return result
self.qtodefs = {}
qtopath = os.path.join(
FreeCAD.getResourceDir(), "Mod", "BIM", "Presets", "qto_definitions.csv"
)
custompath = os.path.join(FreeCAD.getUserAppDataDir(), "BIM", "CustomQtos.csv")
self.qtodefs = read_csv(qtopath)
self.qtodefs.update(read_csv(custompath))
self.qtokeys = [
"".join(map(lambda x: x if x.islower() else " " + x, t[4:]))[1:]
for t in self.qtodefs.keys()
]
self.qtokeys.sort()
self.form.comboQto.addItems(
[translate("BIM", "Add quantity set..."),]
+ self.qtokeys
)
def add_qto(self):
"Adds a standard qto set to the todo list"
index = self.form.comboQto.currentIndex()
if index <= 0:
return
if len(FreeCADGui.Selection.getSelection()) != 1:
return
obj = FreeCADGui.Selection.getSelection()[0]
qto = list(self.qtodefs.keys())[index-1]
self.ifcqtolist.setdefault(obj.Name, []).append(qto)
self.update_line(obj.Name, qto)
FreeCAD.Console.PrintMessage(translate("BIM", "Adding quantity set")+": "+qto+"\n")
def apply_qto(self, obj, qto):
"Adds a standard qto set to the object"
val = self.qtodefs[qto]
qset = None
if hasattr(obj, "StepId"):
from nativeifc import ifc_tools
ifcfile = ifc_tools.get_ifcfile(obj)
element = ifc_tools.get_ifc_element(obj)
if not ifcfile or not element:
return
qset = ifc_tools.api_run("pset.add_qto", ifcfile, product=element, name=qto)
for i in range(0, len(val), 2):
qname = val[i]
qtype = QTO_TYPES[val[i+1]]
if not qname in obj.PropertiesList:
obj.addProperty(qtype, qname, "Quantities", val[i+1])
qval = 0
i = self.get_row(obj.Name)
if i > -1:
for j, p in enumerate(QPROPS):
it = self.qmodel.item(i, j+1)
t = it.text()
if t:
t = t.replace("²","^2").replace("³","^3")
qval = FreeCAD.Units.Quantity(t).Value
if qval:
setattr(obj, qname, qval)
if hasattr(obj, "StepId") and qset:
ifc_tools.api_run("pset.edit_qto", ifcfile, qto=qset, properties={qname: qval})
def update(self, index=None):
"updates the tree widgets in all tabs"
"""updates the tree widgets in all tabs. Index is not used,
it is just there to match a qt slot requirement"""
from PySide import QtCore, QtGui
import Draft
# quantities tab - only fill once
# quantities tab
if not self.quantitiesDrawn:
self.qmodel.setHorizontalHeaderLabels(
[translate("BIM", "Label")] + trqprops
)
quantheaders = self.form.quantities.header() # QHeaderView instance
if hasattr(quantheaders, "setClickable"): # qt4
quantheaders.setClickable(True)
else: # qt5
quantheaders.setSectionsClickable(True)
quantheaders.sectionClicked.connect(self.quantHeaderClicked)
self.qmodel.clear()
self.qmodel.setHorizontalHeaderLabels(
[translate("BIM", "Label")] + TR_QPROPS
)
self.form.quantities.setColumnWidth(0, 200) # TODO remember width
quantheaders = self.form.quantities.header() # QHeaderView instance
quantheaders.setSectionsClickable(True)
quantheaders.sectionClicked.connect(self.quantHeaderClicked)
# sort by type
# sort by type
groups = {}
for name, role in self.objectslist.items():
groups.setdefault(role, []).append(name)
for names in groups.values():
suffix = ""
for name in names:
if "+array" in name:
name = name.split("+array")[0]
suffix = " (duplicate)"
obj = FreeCAD.ActiveDocument.getObject(name)
if obj:
if (
not self.form.onlyVisible.isChecked()
) or obj.ViewObject.isVisible():
if obj.isDerivedFrom("Part::Feature") and not (
Draft.getType(obj) == "Site"
):
it1 = QtGui.QStandardItem(obj.Label + suffix)
it1.setToolTip(name + suffix)
it1.setEditable(False)
if QtCore.QFileInfo(
":/icons/Arch_" + obj.Proxy.Type + "_Tree.svg"
).exists():
icon = QtGui.QIcon(
":/icons/Arch_" + obj.Proxy.Type + "_Tree.svg"
)
else:
icon = QtGui.QIcon(":/icons/Arch_Component.svg")
it1.setIcon(icon)
props = []
for prop in qprops:
it = QtGui.QStandardItem()
val = None
if prop == "Volume":
if obj.Shape and hasattr(obj.Shape, "Volume"):
val = FreeCAD.Units.Quantity(
obj.Shape.Volume, FreeCAD.Units.Volume
)
it.setText(
val.getUserPreferred()[0].replace(
"^3", "³"
)
)
it.setCheckable(True)
else:
if hasattr(obj, prop) and (
not "Hidden" in obj.getEditorMode(prop)
groups = {}
for name, role in self.objectslist.items():
groups.setdefault(role, []).append(name)
for names in groups.values():
suffix = ""
for name in names:
if "+array" in name:
name = name.split("+array")[0]
suffix = " (duplicate)"
obj = FreeCAD.ActiveDocument.getObject(name)
if obj:
if (
not self.form.onlyVisible.isChecked()
) or obj.ViewObject.isVisible():
if obj.isDerivedFrom("Part::Feature") and not (
Draft.getType(obj) == "Site"
):
it1 = QtGui.QStandardItem(obj.Label + suffix)
it1.setToolTip(name + suffix)
it1.setEditable(False)
it1.setIcon(obj.ViewObject.Icon)
props = []
for prop in QPROPS:
it = QtGui.QStandardItem()
val = None
if hasattr(obj, prop) and (
"Hidden" not in obj.getEditorMode(prop)
):
val = self.get_text(obj, prop)
it.setText(val)
it.setCheckable(True)
if val != None:
d = None
if hasattr(obj, "IfcAttributes"):
d = obj.IfcAttributes
elif hasattr(obj, "IfcData"):
d = obj.IfcData
if d:
if ("Export" + prop in d) and (
d["Export" + prop] == "True"
):
val = getattr(obj, prop)
it.setText(
val.getUserPreferred()[0].replace(
"^2", "²"
)
)
it.setCheckable(True)
if val != None:
d = None
if hasattr(obj, "IfcAttributes"):
d = obj.IfcAttributes
elif hasattr(obj, "IfcData"):
d = obj.IfcData
if d:
if ("Export" + prop in d) and (
d["Export" + prop] == "True"
):
it.setCheckState(QtCore.Qt.Checked)
if val == 0:
it.setIcon(
QtGui.QIcon(
os.path.join(
os.path.dirname(__file__),
"icons",
"warning.svg",
)
)
)
if prop in [
"Area",
"HorizontalArea",
"VerticalArea",
"Volume",
]:
it.setEditable(False)
props.append(it)
self.qmodel.appendRow([it1] + props)
self.quantitiesDrawn = True
it.setCheckState(QtCore.Qt.Checked)
elif self.has_qto(obj, prop):
it.setCheckState(QtCore.Qt.Checked)
if val == 0:
it.setIcon(QtGui.QIcon(":/icons/warning.svg"))
self.set_editable(it, prop)
props.append(it)
self.qmodel.appendRow([it1] + props)
def has_qto(self, obj, prop):
"""Says if the given object has the given prop in a qto set"""
if not "StepId" in obj.PropertiesList:
return False
from nativeifc import ifc_tools
element = ifc_tools.get_ifc_element(obj)
if not element:
return False
for rel in getattr(element, "IsDefinedBy", []):
pset = rel.RelatingPropertyDefinition
if pset.is_a("IfcElementQuantity"):
if pset.Name in self.qtodefs:
if prop in self.qtodefs[pset.Name]:
return True
return False
def get_text(self, obj, prop):
"""Gets the text from a property"""
val = getattr(obj, prop, "0")
txt = val.getUserPreferred()[0].replace("^2", "²").replace("^3", "³")
return txt
def get_row(self, name):
"""Returns the row number correspinding to the given object name"""
for i in range(self.qmodel.rowCount()):
if self.qmodel.item(i).toolTip().split(" ")[0] == name:
return i
return -1
def update_line(self, name, qto):
"""Updates a single line of the table, without updating
the actual object"""
from PySide import QtCore, QtGui
i = self.get_row(name)
if i == -1:
return
obj = FreeCAD.ActiveDocument.getObject(name)
qto_val = self.qtodefs[qto]
for j, p in enumerate(QPROPS):
it = self.qmodel.item(i, j+1)
if p in obj.PropertiesList:
val = self.get_text(obj, p)
it.setText(val)
self.set_editable(it, p)
it.setCheckable(True)
elif p in qto_val:
it.setText("0")
it.setCheckable(True)
it.setCheckState(QtCore.Qt.Checked)
self.set_editable(it, p)
def set_editable(self, it, prop):
"""Checks if the given prop should be editable, and sets it"""
if prop in ["Area", "HorizontalArea", "VerticalArea", "Volume"]:
it.setEditable(False)
else:
it.setEditable(True)
def getRole(self, obj):
"""gets the IFC class of this object"""
if hasattr(obj, "IfcType"):
return obj.IfcType
elif hasattr(obj, "IfcRole"):
return obj.IfcRole
elif hasattr(obj, "IfcClass"):
return obj.IfcClass
else:
return None
def accept(self):
"""OK pressed"""
PARAMS.SetInt("BimIfcQuantitiesDialogWidth", self.form.width())
PARAMS.SetInt("BimIfcQuantitiesDialogHeight", self.form.height())
self.form.hide()
changed = False
if self.ifcqtolist:
if not changed:
FreeCAD.ActiveDocument.openTransaction(
"Change quantities"
)
changed = True
for key, val in self.ifcqtolist.items():
obj = FreeCAD.ActiveDocument.getObject(key)
if obj:
for qto in val:
self.apply_qto(obj, qto)
for row in range(self.qmodel.rowCount()):
name = self.qmodel.item(row, 0).toolTip()
obj = FreeCAD.ActiveDocument.getObject(name)
if obj:
for i in range(len(qprops)):
for i in range(len(QPROPS)):
item = self.qmodel.item(row, i + 1)
val = item.text()
sav = bool(item.checkState())
if i < 3: # Length, Width, Height, value can be changed
if hasattr(obj, qprops[i]):
if getattr(obj, qprops[i]).getUserPreferred()[0] != val:
setattr(obj, qprops[i], val)
if hasattr(obj, QPROPS[i]):
if getattr(obj, QPROPS[i]).getUserPreferred()[0] != val:
setattr(obj, QPROPS[i], val)
if not changed:
FreeCAD.ActiveDocument.openTransaction(
"Change quantities"
@@ -277,10 +413,10 @@ class BIM_IfcQuantities:
att = "IfcData"
if d:
if sav:
if (not "Export" + qprops[i] in d) or (
d["Export" + qprops[i]] == "False"
if (not "Export" + QPROPS[i] in d) or (
d["Export" + QPROPS[i]] == "False"
):
d["Export" + qprops[i]] = "True"
d["Export" + QPROPS[i]] = "True"
setattr(obj, att, d)
if not changed:
FreeCAD.ActiveDocument.openTransaction(
@@ -288,16 +424,16 @@ class BIM_IfcQuantities:
)
changed = True
else:
if "Export" + qprops[i] in d:
if d["Export" + qprops[i]] == "True":
d["Export" + qprops[i]] = "False"
if "Export" + QPROPS[i] in d:
if d["Export" + QPROPS[i]] == "True":
d["Export" + QPROPS[i]] = "False"
setattr(obj, att, d)
if not changed:
FreeCAD.ActiveDocument.openTransaction(
"Change quantities"
)
changed = True
else:
elif "StepId" not in obj.PropertiesList:
FreeCAD.Console.PrintError(
translate(
"BIM", "Cannot save quantities settings for object %1"