# SPDX-License-Identifier: LGPL-2.1-or-later # *************************************************************************** # * * # * Copyright (c) 2023 Yorik van Havre * # * * # * This program is free software; you can redistribute it and/or modify * # * it under the terms of the GNU Lesser General Public License (LGPL) * # * as published by the Free Software Foundation; either version 2 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 Library 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 * # * * # *************************************************************************** """This NativeIFC module deals with properties and property sets""" import os import re import FreeCAD from . import ifc_tools translate = FreeCAD.Qt.translate def has_psets(obj): """Returns True if an object has attached psets""" element = ifc_tools.get_ifc_element(obj) psets = getattr(element, "IsDefinedBy", []) if psets and [p for p in psets if p.is_a("IfcRelDefinesByProperties")]: # TODO verify too if these psets are not already there return True psets = getattr(element, "HasProperties", []) if psets: return True return False def get_psets(element): """Returns a dictionary of dictionaries representing the properties of an element in the form: { pset_name : { property_name : IfcType(value), ... }, ... }""" result = {} psets = getattr(element, "IsDefinedBy", []) psets = [p for p in psets if p.is_a("IfcRelDefinesByProperties")] psets = [p.RelatingPropertyDefinition for p in psets] if not psets: psets = getattr(element, "HasProperties", []) if not psets: return result for pset in psets: pset_dict = {} if pset.is_a("IfcPropertySet") and pset.HasProperties: for prop in pset.HasProperties: pset_dict[prop.Name] = str(prop.NominalValue) if pset.is_a("IfcMaterialProperties") and pset.Properties: for prop in pset.Properties: pset_dict[prop.Name] = str(prop.NominalValue) elif pset.is_a("IfcElementQuantity"): # TODO implement quantities pass if pset_dict: result[pset.Name] = pset_dict return result def get_pset(psetname, element): """Returns an IfcPropertySet with the given name""" psets = getattr(element, "IsDefinedBy", []) psets = [p for p in psets if p.is_a("IfcRelDefinesByProperties")] for p in psets: pset = p.RelatingPropertyDefinition if pset.Name == psetname: return pset return None def show_psets(obj): """Shows the psets attached to the given object as properties""" element = ifc_tools.get_ifc_element(obj) if not element: return psets = get_psets(element) for gname, pset in psets.items(): for pname, pvalue in pset.items(): oname = pname ptype, value = pvalue.split("(", 1) value = value.strip(")") value = value.strip("'") pname = re.sub(r"[^0-9a-zA-Z]+", "", pname) if pname[0].isdigit(): pname = "_" + pname ttip = ( ptype + ":" + oname ) # setting IfcType:PropName as a tooltip to desambiguate #while pname in obj.PropertiesList: # print("DEBUG: property", pname, "(", value, ") already exists in", obj.Label) # pname += "_" ftype = None if ptype in [ "IfcPositiveLengthMeasure", "IfcLengthMeasure", "IfcNonNegativeLengthMeasure", ]: ftype = "App::PropertyDistance" elif ptype in ["IfcVolumeMeasure"]: ftype = "App::PropertyVolume" elif ptype in ["IfcPositivePlaneAngleMeasure", "IfcPlaneAngleMeasure"]: ftype = "App::PropertyAngle" value = float(value) while value > 360: value = value - 360 elif ptype in ["IfcMassMeasure"]: ftype = "App::PropertyMass" elif ptype in ["IfcAreaMeasure"]: ftype = "App::PropertyArea" elif ptype in ["IfcCountMeasure", "IfcInteger"]: ftype = "App::PropertyInteger" value = int(value.strip(".")) elif ptype in ["IfcReal"]: ftype = "App::PropertyFloat" value = float(value) elif ptype in ["IfcBoolean", "IfcLogical"]: ftype = "App::PropertyBool" if value in [".T."]: value = True else: value = False elif ptype in [ "IfcDateTime", "IfcDate", "IfcTime", "IfcDuration", "IfcTimeStamp", ]: ftype = "App::PropertyTime" elif isinstance(value, str) and "::" in value: # FreeCAD-specific: split strings by :: delimiter ftype = "App::PropertyStringList" value = value.split("::") else: ftype = "App::PropertyString" # print("DEBUG: setting",pname, ptype, value) if ftype: if pname in obj.PropertiesList \ and obj.getGroupOfProperty(pname) == gname: if obj.getTypeOfProperty(pname) == ftype: pass if ftype == "App::PropertyString" \ and obj.getTypeOfProperty(pname) == "App::PropertyStringList": value = [value] else: print(pname, gname, obj.PropertiesList) obj.addProperty(ftype, pname, gname, ttip) if pname in obj.PropertiesList: setattr(obj, pname, value) def edit_pset(obj, prop, value=None, force=False, ifcfile=None, element=None): """Edits the corresponding property. If force is True, the property is created even if it has no value""" pset = obj.getGroupOfProperty(prop) ptype = obj.getDocumentationOfProperty(prop) if value is None: value = getattr(obj, prop) if not ifcfile: ifcfile = ifc_tools.get_ifcfile(obj) if not ifcfile: return if not element: element = ifc_tools.get_ifc_element(obj) if not element: return pset_exist = get_psets(element) 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: ptype = obj.getTypeIdOfProperty(prop) if ptype == "App::PropertyDistance": ptype = "IfcLengthMeasure" elif ptype == "App::PropertyLength": ptype = "IfcPositiveLengthMeasure" elif ptype == "App::PropertyBool": ptype = "IfcBoolean" elif ptype == "App::PropertyInteger": ptype = "IfcInteger" elif ptype == "App::PropertyFloat": ptype = "IfcReal" elif ptype == "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) 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 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(element.id()) + "): " + 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=ifcpset, properties={target_prop: value} ) # TODO manage quantities return True def load_psets(obj): """Recursively loads psets of child objects""" show_psets(obj) if isinstance(obj, FreeCAD.DocumentObject): group = obj.Group else: group = obj.Objects for child in group: load_psets(child) def add_pset(obj, psetname): """Adds a pset with the given name to the given object""" ifcfile = ifc_tools.get_ifcfile(obj) element = ifc_tools.get_ifc_element(obj) if ifcfile and element: pset = ifc_tools.api_run( "pset.add_pset", ifcfile, product=element, name=psetname ) return pset def add_property(ifcfile, pset, name, value=""): """Adds a property with the given name to the given pset. The type is deduced from the value: string is IfcLabel, True/False is IfcBoolean, number is IfcLengthMeasure. 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 corresponding 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 corresponding 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 . 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) # Quantity types # https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/ifcsharedbldgelements/content.html#6.1.5-Quantity-Sets