BIM: Quantities support for nativeIFC objects (#18689)
* BIM: Quantities support for nativeIFC objects * BIM: Added nativeIFC support for schedules
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user