From 0cbe738e253ae23eea0449059ee993fb934bae81 Mon Sep 17 00:00:00 2001 From: Yorik van Havre Date: Tue, 3 Dec 2024 16:08:27 +0100 Subject: [PATCH] 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 --- src/Mod/BIM/BimStatus.py | 4 + src/Mod/BIM/CMakeLists.txt | 1 + src/Mod/BIM/Presets/properties_conversion.csv | 230 ++++++++++++++++++ src/Mod/BIM/Resources/Arch.qrc | 2 + src/Mod/BIM/Resources/ui/dialogAddPSet.ui | 94 +++++++ src/Mod/BIM/Resources/ui/dialogAddProperty.ui | 142 +++++++++++ src/Mod/BIM/bimcommands/BimIfcProperties.py | 61 +++-- src/Mod/BIM/nativeifc/ifc_observer.py | 7 +- src/Mod/BIM/nativeifc/ifc_psets.py | 202 +++++++++++---- src/Mod/BIM/nativeifc/ifc_status.py | 165 +++++++++++++ src/Mod/BIM/nativeifc/ifc_tools.py | 52 ++-- 11 files changed, 869 insertions(+), 91 deletions(-) create mode 100644 src/Mod/BIM/Presets/properties_conversion.csv create mode 100644 src/Mod/BIM/Resources/ui/dialogAddPSet.ui create mode 100644 src/Mod/BIM/Resources/ui/dialogAddProperty.ui diff --git a/src/Mod/BIM/BimStatus.py b/src/Mod/BIM/BimStatus.py index 0a361999c9..9da009da0d 100644 --- a/src/Mod/BIM/BimStatus.py +++ b/src/Mod/BIM/BimStatus.py @@ -116,6 +116,8 @@ def setStatusIcons(show=True): if show: if statuswidget: statuswidget.show() + if hasattr(statuswidget, "propertybuttons"): + statuswidget.propertybuttons.show() else: statuswidget = FreeCADGui.UiLoader().createWidget("Gui::ToolBar") statuswidget.setObjectName("BIMStatusWidget") @@ -221,3 +223,5 @@ def setStatusIcons(show=True): if statuswidget: statuswidget.hide() statuswidget.toggleViewAction().setVisible(False) + if hasattr(statuswidget, "propertybuttons"): + statuswidget.propertybuttons.hide() diff --git a/src/Mod/BIM/CMakeLists.txt b/src/Mod/BIM/CMakeLists.txt index dedb0c8b03..14e80c6841 100644 --- a/src/Mod/BIM/CMakeLists.txt +++ b/src/Mod/BIM/CMakeLists.txt @@ -82,6 +82,7 @@ SET(Arch_presets Presets/ifc_types_IFC4.json Presets/ifc_contexts_IFC2X3.json Presets/ifc_contexts_IFC4.json + Presets/properties_conversion.csv ) SET(bimcommands_SRCS diff --git a/src/Mod/BIM/Presets/properties_conversion.csv b/src/Mod/BIM/Presets/properties_conversion.csv new file mode 100644 index 0000000000..96309d096c --- /dev/null +++ b/src/Mod/BIM/Presets/properties_conversion.csv @@ -0,0 +1,230 @@ +FreeCAD,IFC +App::PropertyBool,IfcBoolean:IfcLogical +App::PropertyBoolList, +App::PropertyFloat,IfcReal +App::PropertyFloatList, +App::PropertyFloatConstraint, +App::PropertyPrecision, +App::PropertyQuantity, +App::PropertyQuantityConstraint, +App::PropertyInteger,IfcInteger +App::PropertyIntegerConstraint, +App::PropertyPercent, +App::PropertyEnumeration, +App::PropertyIntegerList, +App::PropertyIntegerSet, +App::PropertyMap, +App::PropertyString,IfcLabel:IfcIdentifier:IfcText +App::PropertyPersistentObject, +App::PropertyUUID,IfcGloballyUniqueId +App::PropertyFont,IfcTextFontName +App::PropertyStringList, +App::PropertyLink, +App::PropertyLinkChild, +App::PropertyLinkGlobal, +App::PropertyLinkHidden, +App::PropertyLinkSub, +App::PropertyLinkSubChild, +App::PropertyLinkSubGlobal, +App::PropertyLinkSubHidden, +App::PropertyLinkList, +App::PropertyLinkListChild, +App::PropertyLinkListGlobal, +App::PropertyLinkListHidden, +App::PropertyLinkSubList, +App::PropertyLinkSubListChild, +App::PropertyLinkSubListGlobal, +App::PropertyLinkSubListHidden, +App::PropertyXLink, +App::PropertyXLinkSub, +App::PropertyXLinkSubHidden, +App::PropertyXLinkSubList, +App::PropertyXLinkList, +App::PropertyMatrix, +App::PropertyVector, +App::PropertyVectorDistance, +App::PropertyPosition, +App::PropertyDirection, +App::PropertyVectorList, +App::PropertyPlacement, +App::PropertyPlacementList, +App::PropertyPlacementLink, +App::PropertyRotation, +App::PropertyColor, +App::PropertyColorList, +App::PropertyMaterial, +App::PropertyMaterialList, +App::PropertyPath, +App::PropertyFile, +App::PropertyFileIncluded, +App::PropertyPythonObject, +App::PropertyExpressionEngine, +App::PropertyAcceleration, +App::PropertyAmountOfSubstance, +App::PropertyAngle,IfcPlaneAngleMeasure +App::PropertyArea,IfcAreaMeasure +App::PropertyCompressiveStrength, +App::PropertyCurrentDensity, +App::PropertyDensity, +App::PropertyDissipationRate, +App::PropertyDistance,IfcLengthMeasure +App::PropertyDynamicViscosity, +App::PropertyElectricalCapacitance, +App::PropertyElectricalConductance, +App::PropertyElectricalConductivity, +App::PropertyElectricalInductance, +App::PropertyElectricalResistance, +App::PropertyElectricCharge, +App::PropertyElectricCurrent, +App::PropertyElectricPotential, +App::PropertyFrequency,IfcFrequencyMeasure +App::PropertyForce,IfcForceMeasure +App::PropertyHeatFlux,IfcHeatFluxDensityMeasure +App::PropertyInverseArea, +App::PropertyInverseLength, +App::PropertyInverseVolume, +App::PropertyKinematicViscosity, +App::PropertyLength,IfcPositiveLengthMeasure +App::PropertyLuminousIntensity, +App::PropertyMagneticFieldStrength, +App::PropertyMagneticFlux, +App::PropertyMagneticFluxDensity, +App::PropertyMagnetization, +App::PropertyMass,IfcMassMeasure +App::PropertyMoment,IfcMomentOfInertiaMeasure +App::PropertyPressure,IfcPressureMeasure +App::PropertyPower,IfcPowerMeasure +App::PropertyShearModulus,IfcShearModulusMeasure +App::PropertySpecificEnergy, +App::PropertySpecificHeat, +App::PropertySpeed, +App::PropertyStiffness, +App::PropertyStiffnessDensity, +App::PropertyStress, +App::PropertyTemperature,IfcThermodynamicTemperatureMeasure +App::PropertyThermalConductivity,IfcThermalConductivityMeasure +App::PropertyThermalExpansionCoefficient,IfcThermalExpansionCoefficientMeasure +App::PropertyThermalTransferCoefficient,IfcThermalTransmittanceMeasure +App::PropertyTime,IfcDateTime:IfcDate:IfcTime:IfcTimeMeasure:IfcTimeStamp +App::PropertyUltimateTensileStrength, +App::PropertyVacuumPermittivity, +App::PropertyVelocity, +App::PropertyVolume,IfcVolumeMeasure +App::PropertyVolumeFlowRate, +App::PropertyVolumetricThermalExpansionCoefficient, +App::PropertyWork, +App::PropertyYieldStrength, +App::PropertyYoungsModulus, +Materials::PropertyMaterial, +Part::PropertyPartShape, +Part::PropertyGeometryList, +Part::PropertyShapeHistory, +Part::PropertyFilletEdges, +Part::PropertyShapeCache, +Part::PropertyTopoShapeList, +Sketcher::PropertyConstraintList, +,IfcBSplineCurveForm +,IfcBSplineSurfaceForm +,IfcBooleanOperator +,IfcKnotType +,IfcNullStyle +,IfcPreferredSurfaceCurveRepresentation +,IfcSIPrefix +,IfcSIUnitName +,IfcSurfaceSide +,IfcTextPath +,IfcTransitionCode +,IfcTrimmingPreference +,IfcAbsorbedDoseMeasure +,IfcAccelerationMeasure +,IfcAmountOfSubstanceMeasure +,IfcAngularVelocityMeasure +,IfcAreaDensityMeasure +,IfcBoxAlignment +,IfcCardinalPointReference +,IfcContextDependentMeasure +,IfcCountMeasure +,IfcCurvatureMeasure +,IfcDayInMonthNumber +,IfcDayInWeekNumber +,IfcDescriptiveMeasure +,IfcDimensionCount +,IfcDoseEquivalentMeasure +,IfcDuration +,IfcDynamicViscosityMeasure +,IfcElectricCapacitanceMeasure +,IfcElectricChargeMeasure +,IfcElectricConductanceMeasure +,IfcElectricCurrentMeasure +,IfcElectricResistanceMeasure +,IfcElectricVoltageMeasure +,IfcEnergyMeasure +,IfcFontStyle +,IfcFontVariant +,IfcFontWeight +,IfcHeatingValueMeasure +,IfcIlluminanceMeasure +,IfcInductanceMeasure +,IfcIntegerCountRateMeasure +,IfcIonConcentrationMeasure +,IfcIsothermalMoistureCapacityMeasure +,IfcKinematicViscosityMeasure +,IfcLanguageId +,IfcLinearForceMeasure +,IfcLinearMomentMeasure +,IfcLinearStiffnessMeasure +,IfcLinearVelocityMeasure +,IfcLuminousFluxMeasure +,IfcLuminousIntensityDistributionMeasure +,IfcLuminousIntensityMeasure +,IfcMagneticFluxDensityMeasure +,IfcMagneticFluxMeasure +,IfcMassDensityMeasure +,IfcMassFlowRateMeasure +,IfcMassPerLengthMeasure +,IfcModulusOfElasticityMeasure +,IfcModulusOfLinearSubgradeReactionMeasure +,IfcModulusOfRotationalSubgradeReactionMeasure +,IfcModulusOfSubgradeReactionMeasure +,IfcMoistureDiffusivityMeasure +,IfcMolecularWeightMeasure +,IfcMonetaryMeasure +,IfcMonthInYearNumber +,IfcNonNegativeLengthMeasure +,IfcNormalisedRatioMeasure +,IfcNumericMeasure +,IfcPHMeasure +,IfcParameterValue +,IfcPlanarForceMeasure +,IfcPositiveInteger +,IfcPositivePlaneAngleMeasure +,IfcPositiveRatioMeasure +,IfcPresentableText +,IfcRadioActivityMeasure +,IfcRatioMeasure +,IfcRotationalFrequencyMeasure +,IfcRotationalMassMeasure +,IfcRotationalStiffnessMeasure +,IfcSectionModulusMeasure +,IfcSectionalAreaIntegralMeasure +,IfcSolidAngleMeasure +,IfcSoundPowerLevelMeasure +,IfcSoundPowerMeasure +,IfcSoundPressureLevelMeasure +,IfcSoundPressureMeasure +,IfcSpecificHeatCapacityMeasure +,IfcSpecularExponent +,IfcSpecularRoughness +,IfcTemperatureGradientMeasure +,IfcTemperatureRateOfChangeMeasure +,IfcTextAlignment +,IfcTextDecoration +,IfcTextTransformation +,IfcThermalAdmittanceMeasure +,IfcThermalResistanceMeasure +,IfcTorqueMeasure +,IfcURIReference +,IfcVaporPermeabilityMeasure +,IfcVolumetricFlowRateMeasure +,IfcWarpingConstantMeasure +,IfcWarpingMomentMeasure diff --git a/src/Mod/BIM/Resources/Arch.qrc b/src/Mod/BIM/Resources/Arch.qrc index aef9a36972..48a769697c 100644 --- a/src/Mod/BIM/Resources/Arch.qrc +++ b/src/Mod/BIM/Resources/Arch.qrc @@ -185,6 +185,8 @@ ui/ParametersWindowFixed.svg ui/ParametersWindowSimple.svg ui/ParametersWindowStash.svg + ui/dialogAddPSet.ui + ui/dialogAddProperty.ui ui/dialogClasses.ui ui/dialogClassification.ui ui/dialogConvertDocument.ui diff --git a/src/Mod/BIM/Resources/ui/dialogAddPSet.ui b/src/Mod/BIM/Resources/ui/dialogAddPSet.ui new file mode 100644 index 0000000000..92c46b63bb --- /dev/null +++ b/src/Mod/BIM/Resources/ui/dialogAddPSet.ui @@ -0,0 +1,94 @@ + + + Dialog + + + + 0 + 0 + 310 + 122 + + + + Add standard IFC PSet + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Name + + + + + + + true + + + false + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/Mod/BIM/Resources/ui/dialogAddProperty.ui b/src/Mod/BIM/Resources/ui/dialogAddProperty.ui new file mode 100644 index 0000000000..ffaf8c8c79 --- /dev/null +++ b/src/Mod/BIM/Resources/ui/dialogAddProperty.ui @@ -0,0 +1,142 @@ + + + Dialog + + + + 0 + 0 + 338 + 217 + + + + Add IFC property + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + IfcLabel + + + + + IfcBoolean + + + + + IfcInteger + + + + + IfcReal + + + + + IfcLengthMeasure + + + + + IfcAreaMeasure + + + + + + + + + + + Type + + + + + + + Name + + + + + + + PSet + + + + + + + true + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/Mod/BIM/bimcommands/BimIfcProperties.py b/src/Mod/BIM/bimcommands/BimIfcProperties.py index 01f8f98765..8dde26e648 100644 --- a/src/Mod/BIM/bimcommands/BimIfcProperties.py +++ b/src/Mod/BIM/bimcommands/BimIfcProperties.py @@ -175,6 +175,8 @@ class BIM_IfcProperties: obj.IfcProperties, dict ): props = obj.IfcProperties + elif hasattr(obj, "IfcClass"): + props = self.getNativeIfcProperties(obj) else: props = {} objectslist[obj.Name] = [role, props] @@ -182,6 +184,7 @@ class BIM_IfcProperties: val = val.split(";;") if ";;" in key: # 0.19 format + # pset;;pname = ptype;;pvalue key = key.split(";;") val = [key[1]] + val key = key[0] @@ -222,6 +225,8 @@ class BIM_IfcProperties: return obj.IfcType elif hasattr(obj, "IfcRole"): return obj.IfcRole + elif hasattr(obj, "IfcClass"): + return obj.IfcClass else: return None @@ -259,14 +264,7 @@ class BIM_IfcProperties: obj = FreeCAD.ActiveDocument.getObject(name) if obj: it1 = QtGui.QStandardItem(obj.Label) - 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") + icon = obj.ViewObject.Icon it1.setIcon(icon) it1.setToolTip(obj.Name) it2 = QtGui.QStandardItem(group) @@ -315,12 +313,7 @@ class BIM_IfcProperties: role = self.objectslist[obj.Name][0] if (not self.form.onlyVisible.isChecked()) or obj.ViewObject.isVisible(): it1 = QtGui.QStandardItem(obj.Label) - 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") + icon = obj.ViewObject.Icon it1.setIcon(icon) it1.setToolTip(obj.Name) it2 = QtGui.QStandardItem(role) @@ -353,14 +346,7 @@ class BIM_IfcProperties: not self.form.onlyVisible.isChecked() ) or obj.ViewObject.isVisible(): it1 = QtGui.QStandardItem(obj.Label) - 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") + icon = obj.ViewObject.Icon it1.setIcon(icon) it1.setToolTip(obj.Name) it2 = QtGui.QStandardItem(role) @@ -398,13 +384,28 @@ class BIM_IfcProperties: ) continue props = obj.IfcProperties + elif hasattr(obj, "IfcClass"): + props = self.getNativeIfcProperties(obj) else: props = {} if values[1] != props: if not changed: FreeCAD.ActiveDocument.openTransaction("Change properties") changed = True - if not hasattr(obj, "IfcProperties"): + if hasattr(obj,"IfcClass"): + print("props:",props) + for key,value in values[1].items(): + if ";;" in key and ";;" in value: + pname, pset = key.split(";;") + ptype, pvalue = value.split(";;") + from nativeifc import ifc_psets # lazy loading + fctype = ifc_psets.get_freecad_type(ptype) + if not pname in obj.PropertiesList: + obj.addProperty(fctype, pname, pset, ptype+":"+pname) + ifc_psets.edit_pset(obj, pname, force=True) + if pvalue: + setattr(obj, pname, pvalue) + elif not hasattr(obj, "IfcProperties"): obj.addProperty( "App::PropertyMap", "IfcPRoperties", @@ -413,11 +414,23 @@ class BIM_IfcProperties: "App::Property", "IFC properties of this object" ), ) - obj.IfcProperties = values[1] + if hasattr(obj, "IfcProperties"): + obj.IfcProperties = values[1] if changed: FreeCAD.ActiveDocument.commitTransaction() FreeCAD.ActiveDocument.recompute() + def getNativeIfcProperties(self, obj): + props = {} + for p in obj.PropertiesList: + pset = obj.getGroupOfProperty(p) + ttip = obj.getDocumentationOfProperty(p) + if ":" in ttip: + ptype, pname = ttip.split(":") + if pset not in ["Base", "IFC", "Geometry"]: + props[pname+";;"+pset] = ptype+";;"+str(getattr(obj,p)) + return props + def getSearchResults(self, obj): from PySide import QtCore, QtGui diff --git a/src/Mod/BIM/nativeifc/ifc_observer.py b/src/Mod/BIM/nativeifc/ifc_observer.py index bf4e7adc8c..71638c0ca4 100644 --- a/src/Mod/BIM/nativeifc/ifc_observer.py +++ b/src/Mod/BIM/nativeifc/ifc_observer.py @@ -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): diff --git a/src/Mod/BIM/nativeifc/ifc_psets.py b/src/Mod/BIM/nativeifc/ifc_psets.py index 30b8f0112f..fac71f7135 100644 --- a/src/Mod/BIM/nativeifc/ifc_psets.py +++ b/src/Mod/BIM/nativeifc/ifc_psets.py @@ -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) diff --git a/src/Mod/BIM/nativeifc/ifc_status.py b/src/Mod/BIM/nativeifc/ifc_status.py index a6e1581f96..fc4ad5a68c 100644 --- a/src/Mod/BIM/nativeifc/ifc_status.py +++ b/src/Mod/BIM/nativeifc/ifc_status.py @@ -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): diff --git a/src/Mod/BIM/nativeifc/ifc_tools.py b/src/Mod/BIM/nativeifc/ifc_tools.py index 132eefcb13..da35ef8616 100644 --- a/src/Mod/BIM/nativeifc/ifc_tools.py +++ b/src/Mod/BIM/nativeifc/ifc_tools.py @@ -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):