# SPDX-License-Identifier: LGPL-2.1-or-later # *************************************************************************** # * * # * Copyright (c) 2018 Yorik van Havre * # * * # * This file is part of FreeCAD. * # * * # * FreeCAD is free software: you can redistribute it and/or modify it * # * under the terms of the GNU Lesser General Public License as * # * published by the Free Software Foundation, either version 2.1 of the * # * License, or (at your option) any later version. * # * * # * FreeCAD 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 * # * Lesser General Public License for more details. * # * * # * You should have received a copy of the GNU Lesser General Public * # * License along with FreeCAD. If not, see * # * . * # * * # *************************************************************************** """The BIM Classification command""" import os 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") class BIM_Classification: def GetResources(self): return { "Pixmap": "BIM_Classification", "MenuText": QT_TRANSLATE_NOOP("BIM_Classification", "Manage Classification"), "ToolTip": QT_TRANSLATE_NOOP( "BIM_Classification", "Manages classification systems and apply classification to objects", ), } def IsActive(self): v = hasattr(FreeCADGui.getMainWindow().getActiveWindow(), "getSceneGraph") return v def Activated(self): # only raise the dialog if it is already open if getattr(self, "form", None): self.form.raise_() return import Draft from PySide import QtCore, QtGui from bimcommands import BimMaterial # init checks if not hasattr(self, "Classes"): self.Classes = {} self.isEditing = None current = None # load the form and set the tree model up self.form = FreeCADGui.PySideUic.loadUi(":/ui/dialogClassification.ui") self.form.setWindowIcon(QtGui.QIcon(":/icons/BIM_Classification.svg")) self.form.groupMode.setItemIcon( 0, QtGui.QIcon(":/icons/Arch_SectionPlane_Tree.svg") ) # Alphabetical self.form.groupMode.setItemIcon(1, QtGui.QIcon(":/icons/IFC.svg")) # Type self.form.groupMode.setItemIcon(2, QtGui.QIcon(":/icons/Arch_Material.svg")) # Material self.form.groupMode.setItemIcon(3, QtGui.QIcon(":/icons/Document.svg")) # Model structure # restore saved values self.form.onlyVisible.setChecked(PARAMS.GetInt("BimClassificationVisibleState", 0)) self.form.checkPrefix.setChecked(PARAMS.GetInt("BimClassificationSystemNamePrefix", 1)) w = PARAMS.GetInt("BimClassificationDialogWidth", 629) h = PARAMS.GetInt("BimClassificationDialogHeight", 516) self.form.resize(w, h) # add modified search box from bimmaterial searchBox = BimMaterial.MatLineEdit(self.form) searchBox.setPlaceholderText(translate("BIM", "Search...")) searchBox.setToolTip(translate("BIM", "Searches classes")) self.form.search = searchBox self.form.horizontalLayout_2.addWidget(searchBox) # set help line self.form.labelDownload.setText( self.form.labelDownload.text().replace( "%s", os.path.join(FreeCAD.getUserAppDataDir(), "BIM", "Classification") ) ) # hide materials list if we are editing a particular object if len(FreeCADGui.Selection.getSelection()) == 1: self.isEditing = FreeCADGui.Selection.getSelection()[0] pl = self.isEditing.PropertiesList if ("StandardCode" in pl) or ("IfcClass" in pl): self.form.groupMaterials.hide() self.form.buttonApply.hide() self.form.buttonRename.hide() self.form.setWindowTitle(translate("BIM", "Editing") + " " + self.isEditing.Label) if "IfcClass" in pl: # load existing class if needed from nativeifc import ifc_classification ifc_classification.show_classification(self.isEditing) if "StandardCode" in pl: current = self.isEditing.StandardCode elif "Classification" in self.isEditing.PropertiesList: current = self.isEditing.Classification # fill materials list self.objectslist = {} self.matlist = {} self.labellist = {} for obj in FreeCAD.ActiveDocument.Objects: if "StandardCode" in obj.PropertiesList: if Draft.getType(obj) in ["Material", "MultiMaterial"]: self.matlist[obj.Name] = obj.StandardCode else: self.objectslist[obj.Name] = obj.StandardCode self.labellist[obj.Name] = obj.Label elif "Classification" in obj.PropertiesList: self.objectslist[obj.Name] = obj.Classification self.labellist[obj.Name] = obj.Label # fill objects list if not self.isEditing: self.updateObjects() # fill available classifications p = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Arch").GetString( "DefaultClassificationSystem", "" ) presetdir = os.path.join(FreeCAD.getUserAppDataDir(), "BIM", "Classification") if os.path.isdir(presetdir): presets = [] for f in os.listdir(presetdir): if f.lower().endswith(".xml") or f.lower().endswith(".ifc"): n = os.path.splitext(f)[0] if not n in presets: presets.append(n) self.form.comboSystem.addItem(n) if n == p: self.form.comboSystem.setCurrentIndex(self.form.comboSystem.count() - 1) # connect signals self.form.comboSystem.currentIndexChanged.connect(self.updateClasses) self.form.buttonApply.clicked.connect(self.apply) self.form.buttonRename.clicked.connect(self.rename) self.form.search.textEdited.connect(self.updateClasses) self.form.buttonBox.accepted.connect(self.accept) self.form.rejected.connect(self.reject) # also triggered by self.form.buttonBox.rejected self.form.groupMode.currentIndexChanged.connect(self.updateObjects) self.form.treeClass.itemDoubleClicked.connect(self.apply) self.form.search.up.connect(self.onUpArrow) self.form.search.down.connect(self.onDownArrow) if hasattr(self.form.onlyVisible, "checkStateChanged"): # Qt version >= 6.7.0 self.form.onlyVisible.checkStateChanged.connect(self.onVisible) self.form.checkPrefix.checkStateChanged.connect(self.onPrefix) else: # Qt version < 6.7.0 self.form.onlyVisible.stateChanged.connect(self.onVisible) self.form.checkPrefix.stateChanged.connect(self.onPrefix) # center the dialog over FreeCAD window mw = FreeCADGui.getMainWindow() self.form.move( mw.frameGeometry().topLeft() + mw.rect().center() - self.form.rect().center() ) self.updateClasses() # select current classification if current: system, classification = current.split(" ", 1) print("searching for", classification) if system in self.Classes: self.form.comboSystem.setCurrentText(system) res = self.form.treeClass.findItems( classification, QtCore.Qt.MatchExactly | QtCore.Qt.MatchRecursive, 0 ) if res: self.form.treeClass.setCurrentItem(res[0]) self.form.show() self.form.search.setFocus() def updateObjects(self, idx=None): # store current state of tree into self.objectslist before redrawing for row in range(self.form.treeObjects.topLevelItemCount()): child = self.form.treeObjects.topLevelItem(row) if child.toolTip(0): if child.toolTip(0) in self.objectslist: self.objectslist[child.toolTip(0)] = child.text(1) elif child.toolTip(0) in self.matlist: self.matlist[child.toolTip(0)] = child.text(1) self.labellist[child.toolTip(0)] = child.text(0) for childrow in range(child.childCount()): grandchild = child.child(childrow) if grandchild.toolTip(0): if grandchild.toolTip(0) in self.objectslist: self.objectslist[grandchild.toolTip(0)] = grandchild.text(1) elif grandchild.toolTip(0) in self.matlist: self.matlist[grandchild.toolTip(0)] = grandchild.text(1) self.labellist[grandchild.toolTip(0)] = grandchild.text(0) self.form.treeObjects.clear() if self.form.groupMode.currentIndex() == 1: # group by type self.updateByType() elif self.form.groupMode.currentIndex() == 2: # group by material self.updateByMaterial() elif self.form.groupMode.currentIndex() == 3: # group by model structure self.updateByTree() else: # group alphabetically self.updateDefault() # resize columns - no resizeSection in pyside2 # self.form.treeObjects.header().resizeSection(0,int(self.form.treeObjects.width()/2)) # self.form.treeObjects.header().resizeSection(1,int(self.form.treeObjects.width()/2)) def updateByType(self): from PySide import QtCore, QtGui import Draft groups = {} for name in self.objectslist.keys(): obj = FreeCAD.ActiveDocument.getObject(name) if obj and hasattr(obj, "IfcType"): groups.setdefault(obj.IfcType, []).append(name) elif obj and hasattr(obj, "IfcRole"): groups.setdefault(obj.IfcRole, []).append(name) else: groups.setdefault("Undefined", []).append(name) groups["Materials"] = self.matlist.keys() d = self.objectslist.copy() d.update(self.matlist) for group in groups.keys(): mit = QtGui.QTreeWidgetItem([group, ""]) self.form.treeObjects.addTopLevelItem(mit) for name in groups[group]: obj = FreeCAD.ActiveDocument.getObject(name) if obj: if ( (not self.form.onlyVisible.isChecked()) or obj.ViewObject.isVisible() or (Draft.getType(obj) in ["Material", "MultiMaterial"]) ): it = QtGui.QTreeWidgetItem([self.labellist[name], d[name]]) it.setIcon(0, self.getIcon(obj)) it.setToolTip(0, name) mit.addChild(it) mit.sortChildren(0, QtCore.Qt.AscendingOrder) self.form.treeObjects.expandAll() # self.spanTopLevels() def updateByMaterial(self): from PySide import QtCore, QtGui groups = {} claimed = [] for name in self.matlist.keys(): mat = FreeCAD.ActiveDocument.getObject(name) if mat: children = [par.Name for par in mat.InList if par.Name in self.objectslist.keys()] groups[name] = children claimed.extend(children) groups["Undefined"] = [o for o in self.objectslist.keys() if not o in claimed] for group in groups.keys(): matobj = FreeCAD.ActiveDocument.getObject(group) if matobj: mit = QtGui.QTreeWidgetItem([self.labellist[group], self.matlist[group]]) mit.setIcon(0, self.getIcon(matobj)) mit.setToolTip(0, group) else: mit = QtGui.QTreeWidgetItem(["Undefined", ""]) self.form.treeObjects.addTopLevelItem(mit) for name in groups[group]: obj = FreeCAD.ActiveDocument.getObject(name) if obj: if (not self.form.onlyVisible.isChecked()) or obj.ViewObject.isVisible(): it = QtGui.QTreeWidgetItem([self.labellist[name], self.objectslist[name]]) it.setIcon(0, self.getIcon(obj)) it.setToolTip(0, name) mit.addChild(it) mit.sortChildren(0, QtCore.Qt.AscendingOrder) self.form.treeObjects.expandAll() # self.spanTopLevels() def updateByTree(self): from PySide import QtGui # order by hierarchy def istop(obj): for parent in obj.InList: if parent.Name in self.objectslist.keys(): return False return True rel = [] deps = [] for name in self.objectslist.keys(): obj = FreeCAD.ActiveDocument.getObject(name) if obj: if istop(obj): rel.append(obj) else: deps.append(obj) pa = 1 while deps: for obj in rel: for child in obj.OutList: if child in deps: rel.append(child) deps.remove(child) pa += 1 if pa == 10: # max 10 hierarchy levels, okay? Let's keep civilised rel.extend(deps) break done = {} # materials first mit = QtGui.QTreeWidgetItem(["Materials", ""]) self.form.treeObjects.addTopLevelItem(mit) for name, code in self.matlist.items(): obj = FreeCAD.ActiveDocument.getObject(name) if obj: it = QtGui.QTreeWidgetItem([self.labellist[name], code]) it.setIcon(0, self.getIcon(obj)) it.setToolTip(0, name) mit.addChild(it) # objects next for obj in rel: code = self.objectslist[obj.Name] if (not self.form.onlyVisible.isChecked()) or obj.ViewObject.isVisible(): it = QtGui.QTreeWidgetItem([self.labellist[obj.Name], code]) it.setIcon(0, self.getIcon(obj)) it.setToolTip(0, name) ok = False for par in obj.InListRecursive: if par.Name in done: if (not hasattr(par, "Hosts")) or (obj not in par.Hosts): done[par.Name].addChild(it) done[obj.Name] = it ok = True break if not ok: self.form.treeObjects.addTopLevelItem(it) done[obj.Name] = it self.form.treeObjects.expandAll() def updateDefault(self): from PySide import QtGui import Draft d = self.objectslist.copy() d.update(self.matlist) for name, code in d.items(): obj = FreeCAD.ActiveDocument.getObject(name) if obj: if ( (not self.form.onlyVisible.isChecked()) or obj.ViewObject.isVisible() or (Draft.getType(obj) in ["Material", "MultiMaterial"]) ): it = QtGui.QTreeWidgetItem([self.labellist[name], code]) it.setIcon(0, self.getIcon(obj)) it.setToolTip(0, name) self.form.treeObjects.addTopLevelItem(it) if obj in FreeCADGui.Selection.getSelection(): self.form.treeObjects.setCurrentItem(it) def updateClasses(self, search=""): from PySide import QtGui self.form.treeClass.clear() # save as default FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Arch").SetString( "DefaultClassificationSystem", self.form.comboSystem.currentText() ) if isinstance(search, int): search = "" if self.form.search.text(): search = self.form.search.text() if search: search = search.lower() system = self.form.comboSystem.currentText() if not system: return if not system in self.Classes: self.Classes[system] = self.build(system) if not self.Classes[system]: return for c in self.Classes[system]: it = None first = True if not c[1]: c[1] = "" if search: if (search in c[0].lower()) or (search in c[1].lower()): it = QtGui.QTreeWidgetItem([c[0] + " " + c[1]]) it.setToolTip(0, c[1]) self.form.treeClass.addTopLevelItem(it) else: it = QtGui.QTreeWidgetItem([c[0] + " " + c[1]]) it.setToolTip(0, c[1]) self.form.treeClass.addTopLevelItem(it) if c[2]: self.addChildren(c[2], it, search) if it and first: # select first entry self.form.treeClass.setCurrentItem(it) first = False def addChildren(self, children, parent, search=""): from PySide import QtGui if children: for c in children: it = None if not c[1]: c[1] = "" if search: if (search in c[0].lower()) or (search in c[1].lower()): it = QtGui.QTreeWidgetItem([c[0] + " " + c[1]]) it.setToolTip(0, c[1]) self.form.treeClass.addTopLevelItem(it) else: it = QtGui.QTreeWidgetItem([c[0] + " " + c[1]]) it.setToolTip(0, c[1]) if parent: parent.addChild(it) if c[2]: self.addChildren(c[2], it, search) def build(self, system): # try to load the IFC first preset = os.path.join(FreeCAD.getUserAppDataDir(), "BIM", "Classification", system + ".ifc") if os.path.exists(preset): return self.build_ifc(system) else: preset = os.path.join( FreeCAD.getUserAppDataDir(), "BIM", "Classification", system + ".xml" ) if os.path.exists(preset): return self.build_xml(system) else: FreeCAD.Console.PrintError("Unable to find classification file:" + system + "\n") return [] def build_ifc(self, system): # builds from ifc instead of xml class Item: def __init__(self, parent=None): self.parent = parent self.ID = None self.Name = None self.children = [] preset = os.path.join(FreeCAD.getUserAppDataDir(), "BIM", "Classification", system + ".ifc") if not os.path.exists(preset): return None import ifcopenshell f = ifcopenshell.open(preset) classes = f.by_type("IfcClassificationReference") rootclass = f.by_type("IfcClassification") if rootclass: rootclass = rootclass[0] else: return None root = Item() classdict = {rootclass.id(): root} for cl in classes: currentItem = Item() currentItem.Name = cl.Name currentItem.Description = cl.Description currentItem.ID = cl.Identification if cl.ReferencedSource: if cl.ReferencedSource.id() in classdict: currentItem.parent = classdict[cl.ReferencedSource.id()] classdict[cl.ReferencedSource.id()].children.append(currentItem) classdict[cl.id()] = currentItem return [self.listize(c) for c in root.children] def build_xml(self, system): class Item: def __init__(self, parent=None): self.parent = parent self.ID = None self.Name = None self.children = [] preset = os.path.join(FreeCAD.getUserAppDataDir(), "BIM", "Classification", system + ".xml") if not os.path.exists(preset): return None import codecs import re d = Item() with codecs.open(preset, "r", "utf-8") as f: currentItem = d for l in f: if "" in l: currentItem = Item(currentItem) currentItem.parent.children.append(currentItem) if "" in l: currentItem = currentItem.parent elif currentItem and re.findall(r"(.*?)", l): currentItem.ID = re.findall(r"(.*?)", l)[0] elif currentItem and re.findall(r"(.*?)", l): currentItem.Name = re.findall(r"(.*?)", l)[0] elif ( currentItem and re.findall(r"(.*?)", l) and not currentItem.Name ): currentItem.Name = re.findall("(.*?)", l)[0] return [self.listize(c) for c in d.children] def listize(self, item): return [item.ID, item.Name, [self.listize(it) for it in item.children]] def apply(self, item=None, col=None): if self.form.treeObjects.selectedItems() and len(self.form.treeClass.selectedItems()) == 1: c = self.form.treeClass.selectedItems()[0].text(0) if self.form.checkPrefix.isChecked(): c = self.form.comboSystem.currentText() + " " + c for m in self.form.treeObjects.selectedItems(): if m.toolTip(0): m.setText(1, c) def rename(self): if self.form.treeObjects.selectedItems() and len(self.form.treeClass.selectedItems()) == 1: c = self.form.treeClass.selectedItems()[0].toolTip(0) for m in self.form.treeObjects.selectedItems(): if m.toolTip(0): m.setText(0, c) def accept(self): if not self.isEditing: changed = False for row in range(self.form.treeObjects.topLevelItemCount()): child = self.form.treeObjects.topLevelItem(row) items = [child] items.extend([child.child(childrow) for childrow in range(child.childCount())]) for item in items: code = item.text(1) label = item.text(0) if item.toolTip(0): obj = FreeCAD.ActiveDocument.getObject(item.toolTip(0)) if obj: if hasattr(obj, "StandardCode"): if code != obj.StandardCode: if not changed: FreeCAD.ActiveDocument.openTransaction( "Change standard codes" ) changed = True obj.StandardCode = code elif hasattr(obj, "IfcClass"): if not "Classification" in obj.PropertiesList: obj.addProperty( "App::PropertyString", "Classification", "IFC", locked=True ) if code != obj.Classification: if not changed: FreeCAD.ActiveDocument.openTransaction( "Change standard codes" ) changed = True obj.Classification = code if label != obj.Label: if not changed: FreeCAD.ActiveDocument.openTransaction("Change standard codes") changed = True obj.Label = label if changed: FreeCAD.ActiveDocument.commitTransaction() FreeCAD.ActiveDocument.recompute() else: # Close the form if user has pressed Enter and did not # select anything if len(self.form.treeClass.selectedItems()) < 1: return self.reject() code = self.form.treeClass.selectedItems()[0].text(0) pl = self.isEditing.PropertiesList if ("StandardCode" in pl) or ("IfcClass" in pl): FreeCAD.ActiveDocument.openTransaction("Change standard codes") if self.form.checkPrefix.isChecked(): code = self.form.comboSystem.currentText() + " " + code if "StandardCode" in pl: self.isEditing.StandardCode = code else: if not "Classification" in self.isEditing.PropertiesList: self.isEditing.addProperty( "App::PropertyString", "Classification", "IFC", locked=True ) self.isEditing.Classification = code if hasattr(self.isEditing.ViewObject, "Proxy") and hasattr( self.isEditing.ViewObject.Proxy, "setTaskValue" ): self.isEditing.ViewObject.Proxy.setTaskValue("FieldCode", code) FreeCAD.ActiveDocument.commitTransaction() FreeCAD.ActiveDocument.recompute() p = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/BIM") p.SetInt("BimClassificationDialogWidth", self.form.width()) p.SetInt("BimClassificationDialogHeight", self.form.height()) return self.reject() def reject(self): self.form.hide() del self.form return True def onUpArrow(self): if self.form: i = self.form.treeClass.currentItem() if self.form.treeClass.itemAbove(i): self.form.treeClass.setCurrentItem(self.form.treeClass.itemAbove(i)) def onDownArrow(self): if self.form: i = self.form.treeClass.currentItem() if self.form.treeClass.itemBelow(i): self.form.treeClass.setCurrentItem(self.form.treeClass.itemBelow(i)) def onVisible(self, index): PARAMS.SetInt("BimClassificationVisibleState", getattr(index, "value", index)) self.updateObjects() def onPrefix(self, index): PARAMS.SetInt("BimClassificationSystemNamePrefix", getattr(index, "value", index)) def getIcon(self, obj): """returns a QIcon for an object""" from PySide import QtGui import Arch_rc if hasattr(obj.ViewObject, "Icon"): return obj.ViewObject.Icon elif hasattr(obj.ViewObject, "Proxy") and hasattr(obj.ViewObject.Proxy, "getIcon"): icon = obj.ViewObject.Proxy.getIcon() if icon.startswith("/*"): return QtGui.QIcon(QtGui.QPixmap(icon)) else: return QtGui.QIcon(icon) else: return QtGui.QIcon(":/icons/Arch_Component.svg") FreeCADGui.addCommand("BIM_Classification", BIM_Classification())