Download src/Mod/Material/MaterialEditor.py from AbdulElahGwaith/FreeCAD: direct link, hf CLI and curl.
- Browser
- Download file 39.8 kB
-
https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Mod/Material/MaterialEditor.py
- Command line
-
hf download hf://AbdulElahGwaith/FreeCAD/src/Mod/Material/MaterialEditor.py
-
curl -L -o MaterialEditor.py https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Mod/Material/MaterialEditor.py
39.8 kB
| # SPDX-License-Identifier: LGPL-2.1-or-later | |
| # *************************************************************************** | |
| # * Copyright (c) 2013 Yorik van Havre <yorik@uncreated.net> * | |
| # * Copyright (c) 2019 Bernd Hahnebach <bernd@bimstatik.org> * | |
| # * * | |
| # * 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 * | |
| # * * | |
| # *************************************************************************** | |
| __title__ = "FreeCAD material editor" | |
| __author__ = "Yorik van Havre, Bernd Hahnebach" | |
| __url__ = "https://www.freecad.org" | |
| import os | |
| from pathlib import PurePath | |
| import sys | |
| from PySide import QtCore, QtGui, QtSvgWidgets | |
| import FreeCAD | |
| import FreeCADGui | |
| # import Material_rc | |
| from materialtools.cardutils import get_material_preferred_directory, get_material_preferred_save_directory | |
| # is this still needed after the move to card utils??? | |
| unicode = str | |
| # ************************************************************************************************ | |
| # ************************************************************************************************ | |
| class MaterialEditor: | |
| def __init__(self, obj=None, prop=None, material=None, card_path="", category="Solid"): | |
| """Initializes, optionally with an object name and a material property | |
| name to edit, or directly with a material dictionary.""" | |
| self.obj = obj | |
| self.prop = prop | |
| self.category = category | |
| self.material = material | |
| self.customprops = [] | |
| self.internalprops = [] | |
| self.groups = [] | |
| self.directory = get_material_preferred_directory() | |
| self.save_directory = get_material_preferred_save_directory() | |
| if self.directory is None: | |
| self.directory = FreeCAD.getResourceDir() + "Mod/Material" | |
| self.materials = {} | |
| self.cards = {} | |
| self.icons = {} | |
| self.initialIndex = -1 | |
| self.edited = False | |
| self.card_path = card_path | |
| filePath = os.path.dirname(__file__) + os.sep | |
| self.iconPath = (filePath + "Resources" + os.sep + "icons" + os.sep) | |
| # load the UI file from the same directory as this script | |
| self.widget = FreeCADGui.PySideUic.loadUi(":/ui/materials-editor.ui") | |
| # remove unused Help button | |
| self.widget.setWindowFlags(self.widget.windowFlags() | |
| & ~QtCore.Qt.WindowContextHelpButtonHint) | |
| # restore size and position | |
| param = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Material") | |
| width = param.GetInt("MaterialEditorWidth", 441) | |
| height = param.GetInt("MaterialEditorHeight", 626) | |
| self.widget.resize(width, height) | |
| # additional UI fixes and tweaks | |
| widget = self.widget | |
| buttonURL = widget.ButtonURL | |
| buttonDeleteProperty = widget.ButtonDeleteProperty | |
| buttonAddProperty = widget.ButtonAddProperty | |
| standardButtons = widget.standardButtons | |
| buttonOpen = widget.ButtonOpen | |
| buttonSave = widget.ButtonSave | |
| comboMaterial = widget.ComboMaterial | |
| treeView = widget.treeView | |
| # create preview svg slots | |
| self.widget.PreviewRender = QtSvgWidgets.QSvgWidget(self.iconPath + "preview-rendered.svg") | |
| self.widget.PreviewRender.setMaximumWidth(64) | |
| self.widget.PreviewRender.setMinimumHeight(64) | |
| self.widget.topLayout.addWidget(self.widget.PreviewRender) | |
| self.widget.PreviewVector = QtSvgWidgets.QSvgWidget(self.iconPath + "preview-vector.svg") | |
| self.widget.PreviewVector.setMaximumWidth(64) | |
| self.widget.PreviewVector.setMinimumHeight(64) | |
| self.widget.topLayout.addWidget(self.widget.PreviewVector) | |
| self.updatePreviews(mat=material) | |
| buttonURL.setIcon(QtGui.QIcon(":/icons/internet-web-browser.svg")) | |
| buttonDeleteProperty.setEnabled(False) | |
| standardButtons.button(QtGui.QDialogButtonBox.Ok).setAutoDefault(False) | |
| standardButtons.button(QtGui.QDialogButtonBox.Cancel).setAutoDefault(False) | |
| self.updateCardsInCombo() | |
| # TODO allow one to enter a custom property by pressing Enter in the lineedit | |
| # currently closes the dialog | |
| standardButtons.rejected.connect(self.reject) | |
| standardButtons.button(QtGui.QDialogButtonBox.Ok).clicked.connect(self.verify) | |
| buttonOpen.clicked.connect(self.openfile) | |
| buttonSave.clicked.connect(self.savefile) | |
| buttonURL.clicked.connect(self.openProductURL) | |
| comboMaterial.currentIndexChanged[int].connect(self.chooseMaterial) | |
| buttonAddProperty.clicked.connect(self.addCustomProperty) | |
| buttonDeleteProperty.clicked.connect(self.deleteCustomProperty) | |
| treeView.clicked.connect(self.onClickTree) | |
| model = QtGui.QStandardItemModel() | |
| treeView.setModel(model) | |
| treeView.setUniformRowHeights(True) | |
| treeView.setItemDelegate(MaterialsDelegate()) | |
| model.itemChanged.connect(self.modelChange) | |
| # init model | |
| self.implementModel() | |
| # update the editor with the contents of the property, if we have one | |
| matProperty = None | |
| if self.prop and self.obj: | |
| matProperty = FreeCAD.ActiveDocument.getObject(self.obj).getPropertyByName(self.prop) | |
| elif self.material: | |
| matProperty = self.material | |
| if matProperty: | |
| self.updateMatParamsInTree(matProperty) | |
| self.widget.ComboMaterial.setCurrentIndex(0) | |
| # set after tree params to the none material | |
| if self.card_path: | |
| # we need the index of this path | |
| self.initialIndex = self.widget.ComboMaterial.findData(self.card_path) | |
| self.chooseMaterial(self.initialIndex) | |
| # TODO: What if material and card_name was given? | |
| # In such case ATM material is chosen, give some feedback for all those corner cases. | |
| def implementModel(self): | |
| """implements the model with the material attribute structure.""" | |
| p = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Material") | |
| widget = self.widget | |
| treeView = widget.treeView | |
| model = treeView.model() | |
| model.setHorizontalHeaderLabels(["Property", "Value", "Type"]) | |
| treeView.setColumnWidth(0, 250) | |
| treeView.setColumnWidth(1, 250) | |
| treeView.setColumnHidden(2, True) | |
| from materialtools.cardutils import get_material_template | |
| template_data = get_material_template(True) | |
| for group in template_data: | |
| gg = list(group)[0] # group dict has only one key | |
| top = QtGui.QStandardItem(gg) | |
| model.appendRow([top]) | |
| self.groups.append(gg) | |
| for properName in group[gg]: | |
| pp = properName # property name | |
| item = QtGui.QStandardItem(pp) | |
| item.setToolTip(group[gg][properName]["Description"]) | |
| self.internalprops.append(pp) | |
| it = QtGui.QStandardItem() | |
| it.setToolTip(group[gg][properName]["Description"]) | |
| tt = group[gg][properName]["Type"] | |
| itType = QtGui.QStandardItem(tt) | |
| top.appendRow([item, it, itType]) | |
| treeView.setExpanded(top.index(), p.GetBool("TreeExpand"+gg, True)) | |
| # top.sortChildren(0) | |
| # treeView.expandAll() | |
| self.edited = False | |
| def updateMatParamsInTree(self, data): | |
| """updates the contents of the editor with the given dictionary | |
| the material property keys where added to the editor already | |
| unknown material property keys will be added to the user defined group""" | |
| # print(data) | |
| model = self.widget.treeView.model() | |
| root = model.invisibleRootItem() | |
| for gg in range(root.rowCount() - 1): | |
| group = root.child(gg, 0) | |
| for pp in range(group.rowCount()): | |
| item = group.child(pp, 0) | |
| it = group.child(pp, 1) | |
| kk = self.collapseKey(item.text()) | |
| try: | |
| value = data[kk] | |
| it.setText(value) | |
| del data[kk] | |
| except KeyError: | |
| # treat here changes in Material Card Template | |
| # Norm -> StandardCode | |
| if (kk == "Standard Code") and ("Norm" in data) and data["Norm"]: | |
| it.setText(data["Norm"]) | |
| del data["Norm"] | |
| it.setText("") | |
| userGroup = root.child(gg + 1, 0) | |
| userGroup.setRowCount(0) | |
| self.customprops = [] | |
| for k, i in data.items(): | |
| k = self.expandKey(k) | |
| item = QtGui.QStandardItem(k) | |
| it = QtGui.QStandardItem(i) | |
| userGroup.appendRow([item, it]) | |
| self.customprops.append(k) | |
| self.edited = False | |
| def chooseMaterial(self, index): | |
| if index < 0: | |
| return | |
| if self.verifyMaterial(): | |
| """ | |
| Save any unchanged data | |
| """ | |
| self.edited = False | |
| else: | |
| return | |
| self.card_path = self.widget.ComboMaterial.itemData(index) | |
| FreeCAD.Console.PrintMessage( | |
| "choose_material in material editor:\n" | |
| " {}\n".format(self.card_path) | |
| ) | |
| if os.path.isfile(self.card_path): | |
| from importFCMat import read | |
| d = read(self.card_path) | |
| self.updateMatParamsInTree(d) | |
| # be careful with reading from materials dict | |
| # the card could be updated the dict not | |
| self.widget.ComboMaterial.setCurrentIndex(index) # set after tree params | |
| else: | |
| FreeCAD.Console.PrintError("Material card not found: {}\n".format(self.card_path)) | |
| def verifyMaterial(self): | |
| if self.edited: | |
| reply = QtGui.QMessageBox.question(self.widget, #FreeCADGui.getMainWindow(), | |
| translate("Material","The document has been modified."), | |
| translate("Material","Save changes?"), | |
| QtGui.QMessageBox.Save | QtGui.QMessageBox.Discard | QtGui.QMessageBox.Cancel, | |
| QtGui.QMessageBox.Save) | |
| if reply == QtGui.QMessageBox.Cancel: | |
| return False | |
| if reply == QtGui.QMessageBox.Save: | |
| self.savefile() | |
| return True | |
| def updateCardsInCombo(self): | |
| """updates the contents of the materials combo with existing material cards""" | |
| mat_prefs = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Material/Cards") | |
| sort_by_resources = mat_prefs.GetBool("SortByResources", False) | |
| # get all available materials (fill self.materials, self.cards and self.icons) | |
| from materialtools.cardutils import import_materials as getmats | |
| self.materials, self.cards, self.icons = getmats(category=self.category) | |
| card_name_list = [] # [ [card_name, card_path, icon_path], ... ] | |
| if sort_by_resources is True: | |
| for a_path in sorted(self.materials): | |
| card_name_list.append([self.cards[a_path], a_path, self.icons[a_path]]) | |
| else: | |
| card_names_tmp = {} | |
| for path, name in self.cards.items(): | |
| card_names_tmp[name] = path | |
| for a_name in sorted(card_names_tmp): | |
| a_path = card_names_tmp[a_name] | |
| card_name_list.append([a_name, a_path, self.icons[a_path]]) | |
| card_name_list.insert(0, [None, "", ""]) | |
| self.widget.ComboMaterial.clear() | |
| for mat in card_name_list: | |
| icon_data = mat[2] | |
| if icon_data and isinstance(icon_data, bytes): | |
| byte_array = QtCore.QByteArray(icon_data) | |
| pixmap = QtGui.QPixmap() | |
| if pixmap.loadFromData(byte_array): | |
| icon = QtGui.QIcon(pixmap) | |
| else: | |
| icon = QtGui.QIcon() | |
| elif isinstance(icon_data, str) and icon_data: | |
| # if this is string type, then try to load directly | |
| icon = QtGui.QIcon(icon_data) | |
| else: | |
| # fallback to not crash, empty icon | |
| icon = QtGui.QIcon() | |
| self.widget.ComboMaterial.addItem(icon, mat[0], mat[1]) | |
| def openProductURL(self): | |
| """opens the contents of the ProductURL field in an external browser.""" | |
| model = self.widget.treeView.model() | |
| item = model.findItems(translate("Material", "Product URL"), | |
| QtCore.Qt.MatchRecursive, 0)[0] | |
| group = item.parent() | |
| it = group.child(item.row(), 1) | |
| url = it.text() | |
| if url: | |
| QtGui.QDesktopServices.openUrl(QtCore.QUrl(url, QtCore.QUrl.TolerantMode)) | |
| def modelChange(self, item): | |
| """ | |
| Called when an item in the tree is modified. This will set edited to True, but this | |
| will be reset in the event of mass updates, such as loading a card | |
| """ | |
| self.edited = True | |
| def verify(self, button): | |
| """ | |
| Verify that the user wants to save any changed data before exiting | |
| """ | |
| if self.edited: | |
| reply = QtGui.QMessageBox.question(self.widget, #FreeCADGui.getMainWindow(), | |
| translate("Material","The document has been modified."), | |
| translate("Material","Save changes?"), | |
| QtGui.QMessageBox.Save | QtGui.QMessageBox.Discard | QtGui.QMessageBox.Cancel, | |
| QtGui.QMessageBox.Save) | |
| if reply == QtGui.QMessageBox.Cancel: | |
| return | |
| if reply == QtGui.QMessageBox.Save: | |
| self.savefile() | |
| self.accept() | |
| def accept(self): | |
| "" | |
| self.storeSize() | |
| QtGui.QDialog.accept(self.widget) | |
| def reject(self): | |
| "" | |
| self.storeSize() | |
| QtGui.QDialog.reject(self.widget) | |
| def storeSize(self): | |
| "stores the widget size" | |
| # store widths | |
| p = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Material") | |
| p.SetInt("MaterialEditorWidth", self.widget.width()) | |
| p.SetInt("MaterialEditorHeight", self.widget.height()) | |
| root = self.widget.treeView.model().invisibleRootItem() | |
| for gg in range(root.rowCount()): | |
| group = root.child(gg) | |
| p.SetBool("TreeExpand"+group.text(), self.widget.treeView.isExpanded(group.index())) | |
| def expandKey(self, key): | |
| "adds spaces before caps in a KeyName" | |
| nk = "" | |
| for ln in key: | |
| if ln.isupper(): | |
| if nk: | |
| # this allows for series of caps, such as ProductURL | |
| if not nk[-1].isupper(): | |
| nk += " " | |
| nk += ln | |
| return nk | |
| def collapseKey(self, key): | |
| "removes the spaces in a Key Name" | |
| nk = "" | |
| for ln in key: | |
| if ln != " ": | |
| nk += ln | |
| return nk | |
| def addCustomProperty(self, key=None, value=None): | |
| "Adds a custom property to the editor, optionally with a value." | |
| if not key: | |
| key = self.widget.EditProperty.text() | |
| if key: | |
| model = self.widget.treeView.model() | |
| item = model.findItems(key, QtCore.Qt.MatchRecursive, 0) | |
| if not item: | |
| top = model.findItems(translate("Material", "UserDefined"), | |
| QtCore.Qt.MatchExactly, 0)[0] | |
| item = QtGui.QStandardItem(key) | |
| it = QtGui.QStandardItem(value) | |
| top.appendRow([item, it]) | |
| self.customprops.append(key) | |
| self.edited = True | |
| def deleteCustomProperty(self, key=None): | |
| """Deletes a custom property from the editor, | |
| or deletes the value of an internal property.""" | |
| widget = self.widget | |
| treeView = widget.treeView | |
| model = treeView.model() | |
| buttonDeleteProperty = widget.ButtonDeleteProperty | |
| if not key: | |
| index = treeView.selectedIndexes()[0] | |
| item = model.itemFromIndex(index) | |
| key = item.text() | |
| if key: | |
| item = model.findItems(key, QtCore.Qt.MatchRecursive, 0) | |
| if item: | |
| index = model.indexFromItem(item[0]) | |
| topIndex = index.parent() | |
| top = model.itemFromIndex(topIndex) | |
| row = item[0].row() | |
| if key in self.customprops: | |
| top.takeRow(row) | |
| self.customprops.remove(key) | |
| buttonDeleteProperty.setProperty("text", "Delete property") | |
| elif key in self.internalprops: | |
| it = top.child(row, 1) | |
| it.setText("") | |
| buttonDeleteProperty.setProperty("text", "Delete value") | |
| self.edited = True | |
| buttonDeleteProperty.setEnabled(False) | |
| def onClickTree(self, index): | |
| """Checks if the current item is a custom or an internal property, | |
| and enable the delete property or delete value button.""" | |
| widget = self.widget | |
| buttonDeleteProperty = widget.ButtonDeleteProperty | |
| treeView = widget.treeView | |
| model = treeView.model() | |
| ind = treeView.selectedIndexes()[0] | |
| item = model.itemFromIndex(ind) | |
| text = item.text() | |
| if text in self.customprops: | |
| buttonDeleteProperty.setEnabled(True) | |
| buttonDeleteProperty.setProperty("text", "Delete property") | |
| elif text in self.internalprops: | |
| indParent = ind.parent() | |
| group = model.itemFromIndex(indParent) | |
| row = item.row() | |
| it = group.child(row, 1) | |
| buttonDeleteProperty.setProperty("text", "Delete value") | |
| if it.text(): | |
| buttonDeleteProperty.setEnabled(True) | |
| else: | |
| buttonDeleteProperty.setEnabled(False) | |
| else: | |
| buttonDeleteProperty.setEnabled(False) | |
| buttonDeleteProperty.setProperty("text", "Delete property") | |
| self.updatePreviews() | |
| def getDict(self): | |
| "returns a dictionary from the contents of the editor." | |
| model = self.widget.treeView.model() | |
| if model is None: | |
| return {} | |
| root = model.invisibleRootItem() | |
| d = {} | |
| for gg in range(root.rowCount()): | |
| group = root.child(gg) | |
| for row in range(group.rowCount()): | |
| kk = group.child(row, 0).text() | |
| ii = group.child(row, 1).text() | |
| # TODO the following should be translated back to english | |
| # since text(0) could be translated | |
| matkey = self.collapseKey(str(kk)) | |
| matvalue = unicode(ii) | |
| if matvalue or (matkey == "Name"): | |
| # use only keys which are not empty and the name even if empty | |
| d[matkey] = matvalue | |
| # self.outputDict(d) | |
| return d | |
| def outputDict(self, d): | |
| print("MaterialEditor dictionary") | |
| for param in d: | |
| print(" {} : {}".format(param, d[param])) | |
| """ | |
| def setTexture(self, pattern): | |
| "displays a texture preview if needed" | |
| self.widget.PreviewVector.hide() | |
| if pattern: | |
| try: | |
| import DrawingPatterns | |
| except: | |
| print("DrawingPatterns not found") | |
| else: | |
| pattern = DrawingPatterns.buildFileSwatch(pattern, size=96, png=True) | |
| if pattern: | |
| self.widget.PreviewVector.setPixmap(QtGui.QPixmap(pattern)) | |
| self.widget.PreviewVector.show() | |
| """ | |
| def updatePreviews(self, mat=None): | |
| "updates the preview images from the content of the editor" | |
| if not mat: | |
| mat = self.getDict() | |
| diffcol = None | |
| highlightcol = None | |
| sectioncol = None | |
| if "DiffuseColor" in mat: | |
| diffcol = mat["DiffuseColor"] | |
| elif "ViewColor" in mat: | |
| diffcol = mat["ViwColor"] | |
| elif "Color" in mat: | |
| diffcol = mat["Color"] | |
| if "SpecularColor" in mat: | |
| highlightcol = mat["SpecularColor"] | |
| if "SectionColor" in mat: | |
| sectioncol = mat["SectionColor"] | |
| if diffcol or highlightcol: | |
| fd = QtCore.QFile(self.iconPath + "preview-rendered.svg") | |
| if fd.open(QtCore.QIODevice.ReadOnly | QtCore.QIODevice.Text): | |
| svg = QtCore.QTextStream(fd).readAll() | |
| fd.close() | |
| if diffcol: | |
| svg = svg.replace("#d3d7cf", self.getColorHash(diffcol, val=255)) | |
| svg = svg.replace("#555753", self.getColorHash(diffcol, val=125)) | |
| if highlightcol: | |
| svg = svg.replace("#fffffe", self.getColorHash(highlightcol, val=255)) | |
| self.widget.PreviewRender.load(QtCore.QByteArray(bytes(svg, encoding="utf8"))) | |
| if diffcol or sectioncol: | |
| fd = QtCore.QFile(self.iconPath + "preview-vector.svg") | |
| if fd.open(QtCore.QIODevice.ReadOnly | QtCore.QIODevice.Text): | |
| svg = QtCore.QTextStream(fd).readAll() | |
| fd.close() | |
| if diffcol: | |
| svg = svg.replace("#d3d7cf", self.getColorHash(diffcol, val=255)) | |
| svg = svg.replace("#555753", self.getColorHash(diffcol, val=125)) | |
| if sectioncol: | |
| svg = svg.replace("#ffffff", self.getColorHash(sectioncol, val=255)) | |
| self.widget.PreviewVector.load(QtCore.QByteArray(bytes(svg, encoding="utf8"))) | |
| def getColorHash(self, col, val=255): | |
| "returns a '#000000' string from a '(0.1,0.2,0.3)' string" | |
| col = [float(x.strip()) for x in col.strip("()").split(",")] | |
| color = QtGui.QColor(int(col[0]*val), int(col[1]*val), int(col[2]*val)) | |
| return color.name() | |
| def openfile(self): | |
| if self.verifyMaterial(): | |
| """ | |
| Save any unchanged data | |
| """ | |
| self.edited = False | |
| else: | |
| return | |
| "Opens a FCMat file" | |
| if self.category == "Solid": | |
| directory = self.directory + os.sep + "StandardMaterial" | |
| else: | |
| directory = self.directory + os.sep + "FluidMaterial" | |
| if self.card_path is None or len(self.card_path) == 0: | |
| self.card_path = directory | |
| filetuple = QtGui.QFileDialog.getOpenFileName( | |
| QtGui.QApplication.activeWindow(), | |
| "Open FreeCAD material file", | |
| self.card_path, | |
| "*.FCMat" | |
| ) | |
| if os.path.isfile(filetuple[0]): | |
| card_path_new = filetuple[0] | |
| else: | |
| FreeCAD.Console.PrintError("Error: Invalid path to the material file\n") | |
| return | |
| # we cannot simply execute findData(self.card_path) because e.g. on Windows | |
| # the return path has "/" as folder separator, but the paths in the ComboMaterial | |
| # have also some "\" in them. For example a path can look like this: | |
| # D:/FreeCAD-build/data/Mod\Material\FluidMaterial\Air.FCMat | |
| # To keep it simple, we take a path from the ComboMaterial and change only the | |
| # material card filename | |
| # | |
| # Using the initialIndex variable won't work before a card os selected for the | |
| # first time, so use index 1. Index 0 is a blank entry | |
| if self.widget.ComboMaterial.count() > 1: | |
| path = self.widget.ComboMaterial.itemData(1) | |
| # at first check if we have a uniform usage | |
| # (if a character is not present, rsplit delivers the initial string) | |
| testBackslash = path.rsplit('\\', 1)[0] | |
| testSlash = path.rsplit('/', 1)[0] | |
| if testBackslash == path: | |
| path = testBackslash.rsplit('/', 1)[0] + '/' | |
| elif testSlash == path: | |
| path = testSlash.rsplit('\\', 1)[0] + '\\' | |
| # since we don't know if we have to deal with slash or backslash, take the | |
| # longest result as path | |
| else: | |
| pathBackslash = path.rsplit('\\', 1)[0] | |
| pathSlash = path.rsplit('/', 1)[0] | |
| if len(pathBackslash) > len(pathSlash): | |
| path = pathBackslash + '\\' | |
| else: | |
| path = pathSlash + '/' | |
| # we know that card_path_new has uniformly either / or \ but not yet what | |
| testBackslash = card_path_new.rsplit('\\', 1)[0] | |
| if testBackslash == card_path_new: | |
| self.card_path = path + card_path_new.rsplit('/', 1)[1] | |
| else: | |
| self.card_path = path + card_path_new.rsplit('\\', 1)[1] | |
| index = self.widget.ComboMaterial.findData(self.card_path) | |
| # check if card_path is in known path, means it is in combo box already | |
| # if not print message, and give some feedbach that the card parameter are loaded | |
| if index == -1: | |
| FreeCAD.Console.PrintMessage( | |
| "Card path: {} not found in known cards.\n" | |
| "The material parameter only are loaded.\n" | |
| .format(self.card_path) | |
| ) | |
| # a material card was chosen that is unknown, thus use its full path | |
| self.card_path = card_path_new | |
| from importFCMat import read | |
| materialDict = read(self.card_path) | |
| if materialDict: | |
| self.updateMatParamsInTree(materialDict) | |
| self.widget.ComboMaterial.setCurrentIndex(0) | |
| # set combo box to the none material after tree params | |
| else: | |
| self.chooseMaterial(index) | |
| self.directory = os.path.dirname(self.card_path) | |
| def savefile(self): | |
| "Saves a FCMat file." | |
| model = self.widget.treeView.model() | |
| item = model.findItems(translate("Material", "Name"), | |
| QtCore.Qt.MatchRecursive, 0)[0] | |
| group = item.parent() | |
| it = group.child(item.row(), 1) | |
| name = it.text() | |
| if not name: | |
| name = "Material" | |
| filetuple = QtGui.QFileDialog.getSaveFileName( | |
| QtGui.QApplication.activeWindow(), | |
| "Save FreeCAD material file", | |
| self.save_directory + "/" + name + ".FCMat", | |
| "*.FCMat" | |
| ) | |
| # a tuple of two empty strings returns True, so use the filename directly | |
| filename = filetuple[0] | |
| if filename: | |
| # Update the directories to the current save value | |
| self.save_directory = os.path.dirname(filename) | |
| self.directory = self.save_directory | |
| self.card_path = filename | |
| d = self.getDict() | |
| # self.outputDict(d) | |
| if d: | |
| # Set the card name to match the filename | |
| path = PurePath(filename) | |
| d["CardName"] = path.stem | |
| from importFCMat import write | |
| write(filename, d) | |
| import Materials | |
| # Load the material | |
| manager = Materials.MaterialManager() | |
| manager.getMaterialByPath(filename) | |
| self.edited = False | |
| self.updateCardsInCombo() | |
| # Ensure our card is selected | |
| self.widget.ComboMaterial.setCurrentText(path.stem) | |
| def show(self): | |
| return self.widget.show() | |
| def exec_(self): | |
| return self.widget.exec_() | |
| # ************************************************************************************************ | |
| # ************************************************************************************************ | |
| class MaterialsDelegate(QtGui.QStyledItemDelegate): | |
| """provides display and editing facilities for data items from a model.""" | |
| def __init__(self): | |
| "" | |
| self.matproperty = "" | |
| self.Value = "" | |
| super(MaterialsDelegate, self).__init__() | |
| def createEditor(self, parent, option, index): | |
| """returns the widget used to change data from the model.""" | |
| model = index.model() | |
| column = index.column() | |
| item = model.itemFromIndex(index) | |
| group = item.parent() | |
| if not group: | |
| return | |
| if column == 1: | |
| row = index.row() | |
| PP = group.child(row, 0) | |
| self.matproperty = PP.text().replace(" ", "") # remove spaces | |
| TT = group.child(row, 2) | |
| if TT: | |
| Type = TT.text() | |
| else: | |
| Type = "String" | |
| VV = group.child(row, 1) | |
| self.Value = VV.text() | |
| editor = matProperWidget(parent, self.matproperty, Type, self.Value) | |
| elif column == 0: | |
| if group.text() == "User defined": | |
| editor = matProperWidget(parent) | |
| else: | |
| return | |
| else: | |
| return | |
| return editor | |
| def setEditorData(self, editor, index): | |
| """provides the widget with data to manipulate.""" | |
| Type = editor.property("Type") | |
| model = index.model() | |
| item = model.itemFromIndex(index) | |
| if Type == "Color": | |
| color = editor.property("color") | |
| color = tuple([v/255.0 for v in color.getRgb()]) | |
| item.setText(str(color)) | |
| elif Type == "File": | |
| lineEdit = editor.children()[1] | |
| item.setText(lineEdit.text()) | |
| elif Type == "Float": | |
| # avoid rounding artifacts | |
| inputValue = float('%.6g' % editor.value()) | |
| item.setText(str(inputValue)) | |
| elif Type == "Quantity": | |
| if not hasattr(FreeCAD.Units, self.matproperty): | |
| FreeCAD.Console.PrintError( | |
| "Error: property '{}' is a quantity but has no unit defined\n" | |
| .format(self.matproperty) | |
| ) | |
| return | |
| # we must use the unit of the input value because the | |
| # "Gui::QuantitySpinBox" uses e.g. for the density always the mm-based unit | |
| # kg/mm^3, also when the input value is in kg/^3. | |
| # E.g. when the input is e.g. "7875 kg/m^3" and we would pass "7875" as rawValue | |
| # and "kg/m^3" as unit, we would get "7875 kg/mm^3" as result. If we try to be | |
| # clever and input "7875e-6" as rawValue and "kg/m^3" as unit we get | |
| # "7875e-6 kg/m^3" as result. If we input "7875e-6" as rawValue and "kg/mm^3" | |
| # as unit we get also "7875e-6 kg/m^3" as result. | |
| if not self.Value: | |
| # for empty (not yet set properties) we use the matproperty unit | |
| unit = getattr(FreeCAD.Units, self.matproperty) | |
| quantity = FreeCAD.Units.Quantity(1, unit) | |
| item.setText(str(editor.value()) + " " + quantity.getUserPreferred()[2]) | |
| else: | |
| # since we checked we have a quantity, we can use split() to get the unit | |
| item.setText(str(editor.value()) + " " + self.Value.split()[1]) | |
| else: | |
| super(MaterialsDelegate, self).setEditorData(editor, index) | |
| # ************************************************************************************************ | |
| # ************************************************************************************************ | |
| def matProperWidget(parent=None, matproperty=None, Type="String", Value=None, | |
| minimum=None, maximum=None, stepsize=None, precision=12): | |
| """customs widgets for the material stuff.""" | |
| # FIXME | |
| # Workaround for problem from here: | |
| # https://forum.freecad.org/viewtopic.php?f=18&t=56912&start=20#p516811 | |
| # set precision to 12 | |
| # Better would be a similar system like used in FEM material task panel | |
| # if the value in the InputField has not changed the data is not changed | |
| # why does the value changes if the user clicks in | |
| # the value and unit should exact stay as it is displayed before the user | |
| # clicks in the field | |
| # the user defined properties are of Type String and thus uses a "Gui::PrefLineEdit" | |
| ui = FreeCADGui.UiLoader() | |
| if Type == "String": | |
| widget = ui.createWidget("Gui::PrefLineEdit") | |
| elif Type == "URL": | |
| widget = ui.createWidget("Gui::PrefLineEdit") | |
| elif Type == "File": | |
| widget = ui.createWidget("Gui::FileChooser") | |
| if Value: | |
| lineEdit = widget.children()[1] | |
| lineEdit.setText(Value) | |
| elif Type == "Quantity": | |
| # We don't use a Gui::QuantitySpinBox widget because depending on the value, | |
| # the unit might change. For some inputs there is no way to do this right, | |
| # see the comment above in "def setEditorData". Moreover it is error-prone to provide | |
| # a unit as in the Gui::QuantitySpinBox widget because users likely delete the unit or | |
| # change it accidentally. It is therefore better not to display the unit while editing. | |
| widget = ui.createWidget("Gui::DoubleSpinBox") | |
| if minimum is None: | |
| widget.setMinimum(-1*sys.float_info.max) | |
| else: | |
| widget.setMinimum(minimum) | |
| if maximum is None: | |
| widget.setMaximum(sys.float_info.max) | |
| else: | |
| widget.setMaximum(maximum) | |
| # we must increase the digits before we can set the value | |
| # 6 is sufficient as some metarial cards use e.g. "0.000011" | |
| widget.setDecimals(6) | |
| # for properties with an underscored number (vectorial values), | |
| # we must strip the part after the first underscore to obtain the bound unit | |
| # since in cardutils.py in def get_material_template | |
| # the underscores were removed, we must check for numbers | |
| import re | |
| if re.search(r'\d', matproperty): | |
| matpropertyNum = matproperty.rstrip('0123456789') | |
| matproperty = matpropertyNum | |
| if Value: | |
| widget.setValue(float(Value.split()[0])) | |
| elif Type == "Integer": | |
| widget = ui.createWidget("Gui::UIntSpinBox") | |
| if minimum is None: | |
| widget.setMinimum(0) | |
| else: | |
| widget.setMinimum(minimum) | |
| if maximum is None: | |
| widget.setMaximum(sys.maxsize) | |
| else: | |
| widget.setMaximum(maximum) | |
| elif Type == "Float": | |
| widget = ui.createWidget("Gui::DoubleSpinBox") | |
| # the magnetic permeability is the parameter for which many decimals matter | |
| # the most however, even for this, 6 digits are sufficient | |
| widget.setDecimals(6) | |
| # for almost all Float parameters of materials a step of 1 would be too large | |
| widget.setSingleStep(0.1) | |
| if minimum is None: | |
| widget.setMinimum(sys.float_info.min) | |
| else: | |
| widget.setMinimum(minimum) | |
| if maximum is None: | |
| widget.setMaximum(sys.float_info.max) | |
| else: | |
| widget.setMaximum(maximum) | |
| widget.setValue(float(Value)) | |
| elif Type == "Enumerator": | |
| widget = ui.createWidget("Gui::PrefComboBox") | |
| elif Type == "Boolean": | |
| widget = ui.createWidget("Gui::PrefComboBox") | |
| widget.insertItems(0, ["", "False", "True"]) | |
| elif Type == "Vector": | |
| widget = ui.createWidget("Gui::PrefLineEdit") | |
| elif Type == "Color": | |
| widget = ui.createWidget("Gui::PrefColorButton") | |
| if Value: | |
| value = string2tuple(Value) | |
| color = QtGui.QColor() | |
| color.setRgb(value[0], value[1], value[2], value[3]) | |
| widget.setProperty("color", color) | |
| else: | |
| widget = QtGui.QLineEdit() | |
| widget.setProperty("Type", Type) | |
| widget.setParent(parent) | |
| return widget | |
| def string2tuple(string): | |
| "provisionally" | |
| value = string[1:-1] | |
| value = value.split(",") | |
| value = [int(float(v)*255) for v in value] | |
| value = tuple(value) | |
| return value | |
| def translate(context, text): | |
| "translates text" | |
| return text # TODO use Qt translation mechanism here | |
| def openEditor(obj=None, prop=None): | |
| """openEditor([obj,prop]): opens the editor, optionally with | |
| an object name and material property name to edit""" | |
| # param = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Material/Cards") | |
| # legacy = param.GetBool("LegacyEditor", True) | |
| # if legacy: | |
| # editor = MaterialEditor(obj, prop) | |
| # editor.exec_() | |
| # else: | |
| FreeCADGui.runCommand('Material_Edit',0) | |
| def editMaterial(material=None, card_path=None, category="Solid"): | |
| """editMaterial(material): opens the editor to edit the contents | |
| of the given material dictionary. Returns the modified material dictionary.""" | |
| # if the material editor is opened with this def and the card_path is None | |
| # the combo box with the card name is empty | |
| # this makes sense, because the editor was not opened with a card_path, | |
| # but with material dictionary instead | |
| # TODO: add some text in combo box, may be "custom material data" or "user material data" | |
| # TODO: if card_path is None, all known cards could be checked, | |
| # if one fits exact ALL provided data, this card name could be displayed | |
| editor = MaterialEditor(material=material, card_path=card_path, category=category) | |
| result = editor.exec_() | |
| if result: | |
| return editor.getDict() | |
| else: | |
| # on cancel button an empty dict is returned | |
| return {} | |
| # ************************************************************************************************ | |
| # ************************************************************************************************ | |
| """ | |
| # some examples how to open the material editor in Python: | |
| import MaterialEditor | |
| MaterialEditor.openEditor() | |
| doc = FreeCAD.open( | |
| FreeCAD.ConfigGet("AppHomePath") + "data/examples/FEMExample.FCStd" | |
| ) | |
| import MaterialEditor | |
| MaterialEditor.openEditor("SolidMaterial", "Material") | |
| import MaterialEditor | |
| MaterialEditor.editMaterial({ | |
| "Density": "1234.0 kg/m^3", | |
| "Name": "My-Material-Data", | |
| "PoissonRatio": "0.66", | |
| "YoungsModulus": "123456 MPa" | |
| }) | |
| import MaterialEditor | |
| MaterialEditor.editMaterial("ABS") | |
| """ | |