BIM: Support for property sets in Native IFC (#18067)
* BIM: UI to add properties and psets to IFC objects * BIM: Support native IFC objects in BimProperties * BIM: Support removing IFC properties * BIM: Fixed lint issues
This commit is contained in:
@@ -31,7 +31,7 @@ params = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/NativeIFC")
|
||||
|
||||
|
||||
def add_observer():
|
||||
"""Adds an observer to the running FreeCAD instance"""
|
||||
"""Adds this observer to the running FreeCAD instance"""
|
||||
|
||||
FreeCAD.BIMobserver = ifc_observer()
|
||||
FreeCAD.addDocumentObserver(FreeCAD.BIMobserver)
|
||||
@@ -123,6 +123,11 @@ class ifc_observer:
|
||||
|
||||
ifc_status.on_activate()
|
||||
|
||||
def slotRemoveDynamicProperty(self, obj, prop):
|
||||
|
||||
from nativeifc import ifc_psets
|
||||
ifc_psets.remove_property(obj, prop)
|
||||
|
||||
# implementation methods
|
||||
|
||||
def fit_all(self):
|
||||
|
||||
@@ -23,10 +23,13 @@
|
||||
"""This NativeIFC module deals with properties and property sets"""
|
||||
|
||||
|
||||
import os
|
||||
import re
|
||||
import FreeCAD
|
||||
from nativeifc import ifc_tools
|
||||
|
||||
translate = FreeCAD.Qt.translate
|
||||
|
||||
|
||||
def has_psets(obj):
|
||||
"""Returns True if an object has attached psets"""
|
||||
@@ -148,75 +151,117 @@ def show_psets(obj):
|
||||
setattr(obj, pname, value)
|
||||
|
||||
|
||||
def edit_pset(obj, prop, value=None):
|
||||
"""Edits the corresponding property"""
|
||||
def edit_pset(obj, prop, value=None, force=False):
|
||||
"""Edits the corresponding property. If force is True,
|
||||
the property is created even if it has no value"""
|
||||
|
||||
pset = obj.getGroupOfProperty(prop)
|
||||
ttip = obj.getDocumentationOfProperty(prop)
|
||||
ptype = obj.getDocumentationOfProperty(prop)
|
||||
if value is None:
|
||||
value = getattr(obj, prop)
|
||||
ifcfile = ifc_tools.get_ifcfile(obj)
|
||||
element = ifc_tools.get_ifc_element(obj)
|
||||
pset_exist = get_psets(element)
|
||||
if ttip.startswith("Ifc") and ":" in ttip:
|
||||
target_prop = ttip.split(":", 1)[-1]
|
||||
target_prop = None
|
||||
value_exist = None
|
||||
|
||||
# build prop name and type
|
||||
if ptype.startswith("Ifc"):
|
||||
if ":" in ptype:
|
||||
target_prop = ptype.split(":", 1)[-1]
|
||||
ptype = ptype.split(":", 1)[0]
|
||||
else:
|
||||
# no tooltip set - try to build a name
|
||||
ptype = obj.getTypeIdOfProperty(prop)
|
||||
if ifcprop == "App::PropertyDistance":
|
||||
ptype = "IfcLengthMeasure"
|
||||
elif ifcprop == "App::PropertyLength":
|
||||
ptype = "IfcPositiveLengthMeasure"
|
||||
elif ifcprop == "App::PropertyBool":
|
||||
ptype = "IfcBoolean"
|
||||
elif ifcprop == "App::PropertyInteger":
|
||||
ptype = "IfcInteger"
|
||||
elif ifcprop == "App::PropertyFloat":
|
||||
ptype = "IfcReal"
|
||||
elif ifcprop == "App::PropertyArea":
|
||||
ptype = "IfcAreaMeasure"
|
||||
else:
|
||||
# default
|
||||
ptype = "IfcLabel"
|
||||
if not target_prop:
|
||||
# test if the prop exists under different forms (uncameled, unslashed...)
|
||||
prop = prop.rstrip("_")
|
||||
prop_uncamel = re.sub(r"(\w)([A-Z])", r"\1 \2", prop)
|
||||
prop_unslash = re.sub(r"(\w)([A-Z])", r"\1\/\2", prop)
|
||||
target_prop = None
|
||||
if pset in pset_exist:
|
||||
if not target_prop:
|
||||
if pset in pset_exist:
|
||||
if prop in pset_exist[pset]:
|
||||
target_prop = prop
|
||||
elif prop_uncamel in pset_exist[pset]:
|
||||
target_prop = prop_uncamel
|
||||
elif prop_unslash in pset_exist[pset]:
|
||||
target_prop = prop_unslash
|
||||
if target_prop:
|
||||
value_exist = pset_exist[pset][target_prop].split("(", 1)[1][:-1].strip("'")
|
||||
if value_exist in [".F.", ".U."]:
|
||||
value_exist = False
|
||||
elif value_exist in [".T."]:
|
||||
value_exist = True
|
||||
elif isinstance(value, int):
|
||||
value_exist = int(value_exist.strip("."))
|
||||
elif isinstance(value, float):
|
||||
value_exist = float(value_exist)
|
||||
elif isinstance(value, FreeCAD.Units.Quantity):
|
||||
if value.Unit.Type == "Angle":
|
||||
value_exist = float(value_exist)
|
||||
while value_exist > 360:
|
||||
value_exist = value_exist - 360
|
||||
value_exist = FreeCAD.Units.Quantity(float(value_exist), value.Unit)
|
||||
if value == value_exist:
|
||||
return False
|
||||
else:
|
||||
FreeCAD.Console.PrintLog(
|
||||
"IFC: property changed for "
|
||||
+ obj.Label
|
||||
+ " ("
|
||||
+ str(obj.StepId)
|
||||
+ ") : "
|
||||
+ str(target_prop)
|
||||
+ " : "
|
||||
+ str(value)
|
||||
+ " ("
|
||||
+ str(type(value))
|
||||
+ ") -> "
|
||||
+ str(value_exist)
|
||||
+ " ("
|
||||
+ str(type(value_exist))
|
||||
+ ")\n"
|
||||
)
|
||||
pset = get_pset(pset, element)
|
||||
else:
|
||||
pset = ifc_tools.api_run("pset.add_pset", ifcfile, product=element, name=pset)
|
||||
if not target_prop:
|
||||
target_prop = prop
|
||||
|
||||
# create pset if needed
|
||||
if pset in pset_exist:
|
||||
ifcpset = get_pset(pset, element)
|
||||
if target_prop in pset_exist[pset]:
|
||||
value_exist = pset_exist[pset][target_prop].split("(", 1)[1][:-1].strip("'")
|
||||
else:
|
||||
ifcpset = ifc_tools.api_run("pset.add_pset", ifcfile, product=element, name=pset)
|
||||
|
||||
# value conversions
|
||||
if value_exist in [".F.", ".U."]:
|
||||
value_exist = False
|
||||
elif value_exist in [".T."]:
|
||||
value_exist = True
|
||||
elif isinstance(value, int):
|
||||
if value_exist:
|
||||
value_exist = int(value_exist.strip("."))
|
||||
elif isinstance(value, float):
|
||||
if value_exist:
|
||||
value_exist = float(value_exist)
|
||||
elif isinstance(value, FreeCAD.Units.Quantity):
|
||||
if value_exist:
|
||||
value_exist = float(value_exist)
|
||||
if value.Unit.Type == "Angle":
|
||||
if value_exist:
|
||||
while value_exist > 360:
|
||||
value_exist = value_exist - 360
|
||||
value = value.getValueAs("deg")
|
||||
elif value.Unit.Type == "Length":
|
||||
value = value.getValueAs("mm").Value * ifc_tools.get_scale(ifcfile)
|
||||
else:
|
||||
print("DEBUG: unhandled quantity type:",value, value.Unit.Type)
|
||||
return False
|
||||
if value == value_exist:
|
||||
return False
|
||||
if not force and not value and not value_exist:
|
||||
return False
|
||||
FreeCAD.Console.PrintLog(
|
||||
"IFC: property changed for "
|
||||
+ obj.Label
|
||||
+ " ("
|
||||
+ str(obj.StepId)
|
||||
+ "): "
|
||||
+ str(target_prop)
|
||||
+ ": "
|
||||
+ str(value_exist)
|
||||
+ " ("
|
||||
+ type(value_exist).__name__
|
||||
+ ") -> "
|
||||
+ str(value)
|
||||
+ " ("
|
||||
+ type(value).__name__
|
||||
+ ")\n"
|
||||
)
|
||||
|
||||
# run the change
|
||||
# TODO the property type is automatically determined by ifcopenhell
|
||||
# https://docs.ifcopenshell.org/autoapi/ifcopenshell/api/pset/edit_pset/index.html
|
||||
# and is therefore wrong for Quantity types. Research a way to overcome that
|
||||
ifc_tools.api_run(
|
||||
"pset.edit_pset", ifcfile, pset=pset, properties={target_prop: value}
|
||||
"pset.edit_pset", ifcfile, pset=ifcpset, properties={target_prop: value}
|
||||
)
|
||||
# TODO manage quantities
|
||||
return True
|
||||
@@ -252,3 +297,62 @@ def add_property(ifcfile, pset, name, value=""):
|
||||
To force a certain type, value can also be an IFC element such as IfcLabel"""
|
||||
|
||||
ifc_tools.api_run("pset.edit_pset", ifcfile, pset=pset, properties={name: value})
|
||||
|
||||
|
||||
def get_freecad_type(ptype):
|
||||
"""Returns a FreeCAD property type correspinding to an IFC property type"""
|
||||
|
||||
conv = read_properties_conversion()
|
||||
for key, values in conv.items():
|
||||
if ptype.lower() in [v.lower() for v in values.split(":")]:
|
||||
return key
|
||||
return "App::PropertyString"
|
||||
|
||||
|
||||
def get_ifc_type(fctype):
|
||||
"""Returns an IFC property type correspinding to a FreeCAD property type"""
|
||||
|
||||
conv = read_properties_conversion()
|
||||
for key, values in conv.items():
|
||||
if fctype.lower() == key.lower():
|
||||
return values.split(":")[0]
|
||||
return "IfcLabel"
|
||||
|
||||
|
||||
def read_properties_conversion():
|
||||
"""Reads the properties conversion table"""
|
||||
|
||||
import csv
|
||||
csvfile = os.path.join(
|
||||
FreeCAD.getResourceDir(), "Mod", "BIM", "Presets", "properties_conversion.csv"
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
def remove_property(obj, prop):
|
||||
"""Removes a custom property"""
|
||||
|
||||
from nativeifc import ifc_tools
|
||||
ifcfile = ifc_tools.get_ifcfile(obj)
|
||||
if not ifcfile:
|
||||
return
|
||||
element = ifc_tools.get_ifc_element(obj, ifcfile)
|
||||
if not element:
|
||||
return
|
||||
psets = get_psets(element)
|
||||
for psetname, props in psets.items():
|
||||
if prop in props:
|
||||
pset = get_pset(psetname, element)
|
||||
if pset:
|
||||
FreeCAD.Console.PrintMessage(translate("BIM","Removing property")+": "+prop)
|
||||
ifc_tools.api_run("pset.edit_pset", ifcfile, pset=pset, properties={prop: None})
|
||||
if len(props) == 1:
|
||||
# delete the pset too
|
||||
FreeCAD.Console.PrintMessage(translate("BIM","Removing property set")+": "+psetname)
|
||||
ifc_tools.api_run("pset.remove_pset", ifcfile, product=element, pset=pset)
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
"""This contains nativeifc status widgets and functionality"""
|
||||
|
||||
|
||||
import os
|
||||
import csv
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
|
||||
@@ -56,6 +58,169 @@ def set_status_widget(statuswidget):
|
||||
lock_button.setChecked(checked)
|
||||
on_toggle_lock(checked, noconvert=True)
|
||||
lock_button.triggered.connect(on_toggle_lock)
|
||||
set_properties_editor(statuswidget)
|
||||
|
||||
|
||||
def set_properties_editor(statuswidget):
|
||||
"""Adds additional buttons to the properties editor"""
|
||||
|
||||
if hasattr(statuswidget, "propertybuttons"):
|
||||
statuswidget.propertybuttons.show()
|
||||
else:
|
||||
from PySide import QtCore, QtGui # lazy loading
|
||||
|
||||
mw = FreeCADGui.getMainWindow()
|
||||
editor = mw.findChild(QtGui.QTabWidget,"propertyTab")
|
||||
if editor:
|
||||
pTabCornerWidget = QtGui.QWidget()
|
||||
pButton1 = QtGui.QToolButton(pTabCornerWidget)
|
||||
pButton1.setText("")
|
||||
pButton1.setToolTip(translate("BIM","Add IFC property..."))
|
||||
pButton1.setIcon(QtGui.QIcon(":/icons/IFC.svg"))
|
||||
pButton1.clicked.connect(on_add_property)
|
||||
pButton2 = QtGui.QToolButton(pTabCornerWidget)
|
||||
pButton2.setText("")
|
||||
pButton2.setToolTip(translate("BIM","Add standard IFC Property Set..."))
|
||||
pButton2.setIcon(QtGui.QIcon(":/icons/BIM_IfcProperties.svg"))
|
||||
pButton2.clicked.connect(on_add_pset)
|
||||
pHLayout = QtGui.QHBoxLayout(pTabCornerWidget)
|
||||
pHLayout.addWidget(pButton1)
|
||||
pHLayout.addWidget(pButton2)
|
||||
pHLayout.setSpacing(2)
|
||||
pHLayout.setContentsMargins(2, 2, 0, 0)
|
||||
pHLayout.insertStretch(0)
|
||||
editor.setCornerWidget(pTabCornerWidget, QtCore.Qt.BottomRightCorner)
|
||||
statuswidget.propertybuttons = pTabCornerWidget
|
||||
QtCore.QTimer.singleShot(0,pTabCornerWidget.show)
|
||||
|
||||
|
||||
def on_add_property():
|
||||
"""When the 'add property' button is clicked"""
|
||||
|
||||
sel = FreeCADGui.Selection.getSelection()
|
||||
if not sel:
|
||||
return
|
||||
from PySide import QtCore, QtGui # lazy loading
|
||||
from nativeifc import ifc_psets
|
||||
obj = sel[0]
|
||||
psets = list(set([obj.getGroupOfProperty(p) for p in obj.PropertiesList]))
|
||||
psets = [p for p in psets if p]
|
||||
psets = [p for p in psets if p not in ["Base", "IFC", "Geometry"]]
|
||||
mw = FreeCADGui.getMainWindow()
|
||||
editor = mw.findChild(QtGui.QTabWidget,"propertyTab")
|
||||
pset = None
|
||||
if editor:
|
||||
wid = editor.currentWidget()
|
||||
if wid and wid.objectName() == "propertyEditorData":
|
||||
if wid.currentIndex().parent():
|
||||
pset = wid.currentIndex().parent().data()
|
||||
else:
|
||||
pset = wid.currentIndex().data()
|
||||
form = FreeCADGui.PySideUic.loadUi(":/ui/dialogAddProperty.ui")
|
||||
# center the dialog over FreeCAD window
|
||||
form.move(mw.frameGeometry().topLeft() + mw.rect().center() - form.rect().center())
|
||||
form.field_pset.clear()
|
||||
form.field_pset.addItems(psets)
|
||||
if pset and (pset in psets):
|
||||
form.field_pset.setCurrentIndex(psets.index(pset))
|
||||
# TODO check for name duplicates while typing
|
||||
# execute
|
||||
result = form.exec_()
|
||||
if not result:
|
||||
return
|
||||
pname = form.field_name.text()
|
||||
if pname in obj.PropertiesList:
|
||||
print("DEBUG: property already exists",pname)
|
||||
return
|
||||
pset = form.field_pset.currentText()
|
||||
if not pset:
|
||||
# TODO disable the OK button if empty
|
||||
t = translate("BIM","No Property set provided")
|
||||
FreeCAD.Console.PrintError(t+"\n")
|
||||
ptype = form.field_type.currentIndex()
|
||||
ptype = ["IfcLabel", "IfcBoolean",
|
||||
"IfcInteger", "IfcReal",
|
||||
"IfcLengthMeasure", "IfcAreaMeasure"][ptype]
|
||||
fctype = ifc_psets.get_freecad_type(ptype)
|
||||
FreeCAD.ActiveDocument.openTransaction(translate("BIM","add property"))
|
||||
for obj in sel:
|
||||
obj.addProperty(fctype, pname, pset, ptype+":"+pname)
|
||||
ifc_psets.edit_pset(obj, pname, force=True)
|
||||
FreeCAD.ActiveDocument.commitTransaction()
|
||||
|
||||
|
||||
def on_add_pset():
|
||||
"""When the 'add pset' button is pressed"""
|
||||
|
||||
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
|
||||
|
||||
def get_fcprop(ifcprop):
|
||||
if ifcprop == "IfcLengthMeasure":
|
||||
return "App::PropertyDistance"
|
||||
elif ifcprop == "IfcPositiveLengthMeasure":
|
||||
return "App::PropertyLength"
|
||||
elif ifcprop in ["IfcBoolean", "IfcLogical"]:
|
||||
return "App::PropertyBool"
|
||||
elif ifcprop == "IfcInteger":
|
||||
return "App::PropertyInteger"
|
||||
elif ifcprop == "IfcReal":
|
||||
return "App::PropertyFloat"
|
||||
elif ifcprop == "IfcAreaMeasure":
|
||||
return "App::PropertyArea"
|
||||
return "App::PropertyString"
|
||||
|
||||
sel = FreeCADGui.Selection.getSelection()
|
||||
if not sel:
|
||||
return
|
||||
from PySide import QtCore, QtGui # lazy loading
|
||||
from nativeifc import ifc_psets
|
||||
obj = sel[0]
|
||||
mw = FreeCADGui.getMainWindow()
|
||||
# read standard psets
|
||||
psetpath = os.path.join(
|
||||
FreeCAD.getResourceDir(), "Mod", "BIM", "Presets", "pset_definitions.csv"
|
||||
)
|
||||
custompath = os.path.join(FreeCAD.getUserAppDataDir(), "BIM", "CustomPsets.csv")
|
||||
psetdefs = read_csv(psetpath)
|
||||
psetdefs.update(read_csv(custompath))
|
||||
psetkeys = list(psetdefs.keys())
|
||||
psetkeys.sort()
|
||||
form = FreeCADGui.PySideUic.loadUi(":/ui/dialogAddPSet.ui")
|
||||
# center the dialog over FreeCAD window
|
||||
form.move(mw.frameGeometry().topLeft() + mw.rect().center() - form.rect().center())
|
||||
form.field_pset.clear()
|
||||
form.field_pset.addItems(psetkeys)
|
||||
# execute
|
||||
result = form.exec_()
|
||||
if not result:
|
||||
return
|
||||
pset = form.field_pset.currentText()
|
||||
existing_psets = list(set([obj.getGroupOfProperty(p) for p in obj.PropertiesList]))
|
||||
if pset in existing_psets:
|
||||
t = translate("BIM","Property set already exists")
|
||||
FreeCAD.Console.PrintError(t+": "+pset+"\n")
|
||||
return
|
||||
props = [psetdefs[pset][i:i+2] for i in range(0, len(psetdefs[pset]), 2)]
|
||||
props = [[p[0], p[1]] for p in props]
|
||||
FreeCAD.ActiveDocument.openTransaction(translate("BIM","add property set"))
|
||||
for obj in sel:
|
||||
existing_psets = list(set([obj.getGroupOfProperty(p) for p in obj.PropertiesList]))
|
||||
if pset not in existing_psets:
|
||||
ifc_psets.add_pset(obj, pset)
|
||||
for prop in props:
|
||||
if prop[0] in obj.PropertiesList:
|
||||
t = translate("BIM","Property already exists")
|
||||
FreeCAD.Console.PrintWarning(t+": "+obj.Label+","+prop[0]+"\n")
|
||||
else:
|
||||
obj.addProperty(get_fcprop(prop[1]),prop[0],pset,prop[1]+":"+prop[0])
|
||||
FreeCAD.ActiveDocument.commitTransaction()
|
||||
|
||||
|
||||
def on_toggle_lock(checked=None, noconvert=False, setchecked=False):
|
||||
|
||||
@@ -745,6 +745,21 @@ def set_attribute(ifcfile, element, attribute, value):
|
||||
|
||||
# This function can become pure IFC
|
||||
|
||||
def differs(val1, val2):
|
||||
if val1 == val2:
|
||||
return False
|
||||
if not val1 and not 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):
|
||||
@@ -774,7 +789,7 @@ def set_attribute(ifcfile, element, attribute, value):
|
||||
):
|
||||
# do not consider default FreeCAD names given to unnamed alements
|
||||
return False
|
||||
if getattr(element, attribute) != value:
|
||||
if differs(getattr(element, attribute, None),value):
|
||||
FreeCAD.Console.PrintLog(
|
||||
"Changing IFC attribute value of "
|
||||
+ str(attribute)
|
||||
@@ -802,22 +817,25 @@ def set_colors(obj, colors):
|
||||
else:
|
||||
colors = [abs(c) for c in colors]
|
||||
if hasattr(vobj, "ShapeColor"):
|
||||
if isinstance(colors[0], (tuple, list)):
|
||||
vobj.ShapeColor = colors[0][:3]
|
||||
# do not set transparency when the object has more than one color
|
||||
#if len(colors[0]) > 3:
|
||||
# vobj.Transparency = int(colors[0][3] * 100)
|
||||
else:
|
||||
vobj.ShapeColor = colors[:3]
|
||||
if len(colors) > 3:
|
||||
vobj.Transparency = int(colors[3] * 100)
|
||||
if hasattr(vobj, "DiffuseColor"):
|
||||
# strip out transparency value because it currently gives ugly
|
||||
# results in FreeCAD when combining transparent and non-transparent objects
|
||||
if all([len(c) > 3 and c[3] != 0 for c in colors]):
|
||||
vobj.DiffuseColor = colors
|
||||
else:
|
||||
vobj.DiffuseColor = [c[:3] for c in colors]
|
||||
# 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:
|
||||
#colors[0] = colors[0][:3] + (0.0,)
|
||||
# TEMP HACK: if multiple colors, set everything to opaque because it looks wrong
|
||||
colors = [color[:3] + (0.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 = color[3] if len(color) > 3 else 0.0
|
||||
sapp.append(sapp_mat)
|
||||
#print(vobj.Object.Label,[[m.DiffuseColor,m.Transparency] for m in sapp])
|
||||
vobj.ShapeAppearance = sapp
|
||||
|
||||
|
||||
def get_body_context_ids(ifcfile):
|
||||
|
||||
Reference in New Issue
Block a user