Description |
---|
This macro helps batch converting to mesh and exporting STL and OBJ files Macro version: 2.16 Last modified: 2024-10-11 Author: Pablo Gil Fernández |
Author |
Pablo Gil Fernández |
Download |
None |
Links |
Macros recipes How to install macros How to customize toolbars |
Macro Version |
2.16 |
Date last modified |
2024-10-11 |
FreeCAD Version(s) |
None |
Default shortcut |
None |
See also |
None |
This macro helps batch converting to mesh and exporting STL and OBJ files. It adds a GUI for speeding up the conversion and file saving of selected objects.
__Title__ = "Batch export to mesh" __Author__ = "Pablo Gil Fernández" __Version__ = "2.16" __Date__ = "2024-10-11" __Comment__ = "This macro helps batch converting to mesh and exporting STL and OBJ files" __Web__ = "https://github.com/pgilfernandez/FreeCAD_Macro_Batch_Export_To_Mesh" __Wiki__ = "https://wiki.freecad.org/Macro_Batch_Export_To_Mesh" __Icon__ = "/usr/lib/freecad/Mod/Mesh/Resources/icons/MeshWorkbench.svg" # __IconW__ = "C:/Documents and Settings/YourUserName/Application Data/FreeCAD" __Help__ = "https://forum.freecadweb.org/viewtopic.php?f=22&t=41219" __Status__ = "stable" __Requires__ = "FreeCAD 0.17" __Communication__ = "https://github.com/pgilfernandez/FreeCAD_Macro_Batch_Export_To_Mesh" import FreeCAD, FreeCADGui, Draft, Part, PartGui, math, PartDesignGui, Mesh, MeshPart, Import, os from FreeCAD import Base from PySide import QtGui, QtCore from math import cos, radians from pivy import coin import sys import subprocess import Mesh App = FreeCAD Gui = FreeCADGui # If developer mode is True it will show important messages on the Report panel: dev_mode = False class SavePresetDialog(QtGui.QDialog): def __init__(self, parent=None): super(SavePresetDialog, self).__init__(parent) self.setWindowTitle("Save Preset") self.layout = QtGui.QVBoxLayout() self.label = QtGui.QLabel("Enter new preset name:") self.layout.addWidget(self.label) self.nameEdit = QtGui.QLineEdit() self.layout.addWidget(self.nameEdit) self.buttonBox = QtGui.QDialogButtonBox(QtCore.Qt.Horizontal) self.saveButton = QtGui.QPushButton("Save") self.cancelButton = QtGui.QPushButton("Cancel") self.buttonBox.addButton(self.saveButton, QtGui.QDialogButtonBox.AcceptRole) self.buttonBox.addButton(self.cancelButton, QtGui.QDialogButtonBox.RejectRole) self.layout.addWidget(self.buttonBox) self.setLayout(self.layout) self.saveButton.clicked.connect(self.accept) self.cancelButton.clicked.connect(self.reject) def getName(self): return self.nameEdit.text().strip() class BatchExportToMesh(QtGui.QDockWidget): def __init__(self): if dev_mode: self.clear_console() existing_instance = Gui.getMainWindow().findChild(QtGui.QWidget, "BatchETM") if existing_instance: if dev_mode: self.message("Old instance closed!") existing_instance.close() # close instance super(BatchExportToMesh, self).__init__() self.setWindowTitle("Batch export to mesh") self.setObjectName("BatchETM") self.setWindowIcon(QtGui.QIcon(":/icons/Mesh_SplitComponents.svg")) self.resize(420, 500) self.paramGet = App.ParamGet("User parameter:BaseApp/BatchETM") self.initParameters() self.initGui() # self.loadConfigurations() # self.updateConfigDropdown() self.loadActiveConfiguration() # Cargar la configuración activa al iniciar self.setAttribute(QtCore.Qt.WA_DeleteOnClose, True) Gui.getMainWindow().addDockWidget(QtCore.Qt.RightDockWidgetArea, self) self.setFloating(True) self.topLevelChanged.connect(self.on_topLevelChanged) # resize window to defaults when undocking self.setFocusOnInput() def setFocusOnInput(self): self.configDropdown.setFocus() def initParameters(self): self.paramGet = App.ParamGet("User parameter:BaseApp/BatchETM") configsGroup = self.paramGet.GetGroup("Configurations") groupNames = configsGroup.GetGroups() # groupNames es una lista de nombres de grupos (cadenas) if not groupNames: if dev_mode: self.message("Creando configuración por defecto") # Crear configuración por defecto self.createDefaultConfiguration() # Cargar configuraciones self.loadConfigurations() def loadConfigurations(self): self.configurations = [] configsGroup = self.paramGet.GetGroup("Configurations") if configsGroup is None: # Crear el grupo 'Configurations' si no existe configsGroup = self.paramGet.GetGroup("Configurations") if dev_mode: self.message("Creado el grupo 'Configurations'") groupNames = configsGroup.GetGroups() # Ahora groupNames es una lista de nombres de grupos (cadenas) if not groupNames: # Si no existen configuraciones, crear una configuración por defecto self.createDefaultConfiguration() else: for groupName in groupNames: configGroup = configsGroup.GetGroup(groupName) configName = configGroup.GetString("ConfigName") if not configName: configName = configGroup.GetString("Name") self.configurations.append((groupName, configName)) if dev_mode: self.message("Configuraciones cargadas: " + str(self.configurations)) def createDefaultConfiguration(self): configsGroup = self.paramGet.GetGroup("Configurations") defaultConfig = configsGroup.GetGroup("config_1") defaultConfig.SetString("ConfigName", "Default") defaultConfig.SetString("Name", "Default") defaultConfig.SetFloat("SurfaceDeviation", 0.10) defaultConfig.SetBool("Relative", False) defaultConfig.SetFloat("AngularDeviation", 5.0) defaultConfig.SetString("FileNameOption", "Label") defaultConfig.SetBool("LocalCoordinates", True) defaultConfig.SetString("FileFormat", "Stereolithography (.STL)") defaultConfig.SetString("FileSavingDirectory", "./") # Establecer ActiveConfig solo al crear la configuración por defecto por primera vez self.paramGet.SetInt("ActiveConfig", 1) # self.configurations.append(("config_1", "Default")) if dev_mode: self.message("Creada configuración por defecto") def on_topLevelChanged(self, floating): if floating: self.resize(420, 500) def initGui(self): infoIcon = [ "16 16 3 1", " c None", "+ c #444444", ". c #e6e6e6", " .... ", " ........ ", " .....++... ", " ......++.... ", " .............. ", " ....+++++..... ", " ...+++++...... ", ".......++.......", ".......++.......", " ......+....... ", " .....++....... ", " .....++.+..... ", " ....++++.... ", " ....++.... ", " ........ ", " .... " ] folderIcon = [ "16 16 3 1", " c None", "+ c #444444", ". c #e6e6e6", " ", " ..... ", "....... ", "........ ", ".............. ", ".............. ", "... ", "... ", ".. ............", ".. ............", ". ............ ", ". ............ ", " ............ ", " ............ ", " ............ ", " " ] deleteIcon = [ "16 16 3 1", " c None", "+ c #444444", ". c #e6e6e6", " ", " ", " ", " ", " ", " ", " ", " ............ ", " ............ ", " ", " ", " ", " ", " ", " ", " " ] addIcon = [ "16 16 3 1", " c None", "+ c #444444", ". c #e6e6e6", " ", " ", " .. ", " .. ", " .. ", " .. ", " .. ", " ............ ", " ............ ", " .. ", " .. ", " .. ", " .. ", " .. ", " ", " " ] # Hide/show "to column" label and spinbox based on mode type def disableWidget(): if self.d7c.isChecked(): self.d7c.setText("Custom:") self.d4.show() self.d4.setEnabled(True) if len(Gui.Selection.getSelection()) == 0: self.d4.setText("mesh001") else: self.d4.setText(Gui.Selection.getSelection()[0].Label) else: self.d7c.setText("Custom") self.d4.setEnabled(False) self.d4.setText("") self.d4.hide() self.dockWidgetContents = QtGui.QWidget() self.dockWidgetContents.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) self.dockWidgetContents.setObjectName("dockWidgetContents") self.grid = QtGui.QGridLayout(self.dockWidgetContents) self.grid.setSpacing(10) iN1 = QtGui.QLabel("Surface deviation") iN1.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) self.d1 = QtGui.QDoubleSpinBox() self.d1.setValue(0.10) self.d1.setSingleStep(0.01) self.d1.setToolTip("Maximal linear deflection of a mesh section from the surface of the object") self.d1.setSuffix(" mm") iN2 = QtGui.QLabel("Angular deviation") iN2.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) self.d2 = QtGui.QDoubleSpinBox() self.d2.setValue(5.0) self.d2.setSingleStep(1.0) self.d2.setToolTip("Maximal angular deflection of a mesh section to the next section") self.d2.setSuffix(" º") iN3 = QtGui.QLabel("Relative") iN3.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) self.d3 = QtGui.QCheckBox() self.d3.setToolTip("The maximal linear deviation of a mesh segment will be specified\nSurface deviation multiplied by the length of the current mesh segment (edge)") self.d4 = QtGui.QLineEdit() self.d4.setText("") # Default value self.d4.setToolTip("Name without extension. For batch export multiple objects it is recommended to add '001' to the name so that the rest of files are numbered correctly.") self.d4.setEnabled(False) # set initial state as disabled self.d4.hide() iN5 = QtGui.QLabel("File format") iN5.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) self.d5 = QtGui.QComboBox() self.d5.addItem("Stereolithography (.STL)") self.d5.addItem("Alias mesh (.OBJ)") self.d5.setToolTip("File format to export") self.d5.setCurrentIndex(0) # sets default option iN6 = QtGui.QLabel("File Saving Directory") iN6.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) self.d6 = QtGui.QLineEdit() self.d6.setText("./") # Default value self.d6.setToolTip("Use any absolute path you want or use relative ones taking in count that:\n - start with './' for relative paths from your project directory.\n - use '../' to exit a folder.") iN7 = QtGui.QLabel("File name:") iN7.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) self.d7a = QtGui.QRadioButton("Label") self.d7a.setChecked(True) self.d7a.setToolTip("if selected, the .STL file will be named with the user defined name") self.d7a.toggled.connect(disableWidget) self.d7b = QtGui.QRadioButton("Project") self.d7b.setChecked(False) self.d7b.setToolTip("if selected, the .STL file will be named with the project name, that is, the FreeCAD file name") self.d7b.toggled.connect(disableWidget) self.d7c = QtGui.QRadioButton("Custom") self.d7c.setChecked(False) self.d7c.setToolTip("if selected, the .STL file will be named with a custom name set in the next field") self.d7c.toggled.connect(disableWidget) # Agrupar los botones de radio self.radio_group = QtGui.QButtonGroup() self.radio_group.addButton(self.d7a) self.radio_group.addButton(self.d7b) self.radio_group.addButton(self.d7c) iN8 = QtGui.QLabel("Use local coordinates") iN8.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) self.d8 = QtGui.QCheckBox() self.d8.setChecked(True) self.d8.setToolTip("if TRUE the .STL origin will match with the part one. It's a very useful option when you design multipart files or assemblies") # Presets iN9 = QtGui.QLabel("Preset:") iN9.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) self.configDropdown = QtGui.QComboBox() self.configDropdown.setToolTip("Select a saved preset to change all the macro parameters. You can save them as new presets whenever you want with the button to the left.") self.updateConfigDropdown() self.configDropdown.currentIndexChanged.connect(self.onConfigSelected) self.deleteConfigButton = QtGui.QPushButton() # "Delete" button deleteSVG = QtGui.QIcon(':/icons/list-remove.svg') if not deleteSVG.isNull(): self.deleteConfigButton.setIcon(deleteSVG) else: self.deleteConfigButton.setIcon(QtGui.QIcon(QtGui.QPixmap(deleteIcon))) self.deleteConfigButton.setToolTip("Delete active preset.") self.deleteConfigButton.clicked.connect(self.onDeleteConfig) self.savePresetButton = QtGui.QPushButton() # "New..." button addSVG = QtGui.QIcon(':/icons/list-add.svg') if not addSVG.isNull(): self.savePresetButton.setIcon(addSVG) else: self.savePresetButton.setIcon(QtGui.QIcon(QtGui.QPixmap(addIcon))) self.savePresetButton.setToolTip("Save parameters into a new preset.") self.savePresetButton.clicked.connect(self.openSavePresetDialog) self.SavePath = QtGui.QPushButton() folderSVG = QtGui.QIcon(':/icons/folder.svg') if not folderSVG.isNull(): self.SavePath.setIcon(folderSVG) else: self.SavePath.setIcon(QtGui.QIcon(QtGui.QPixmap(folderIcon))) self.SavePath.setObjectName("save_path") self.SavePath.setToolTip("Sets a directory to save the converted files. By default it saves the files in the same directory as the FreeCAD project is placed") self.SavePath.clicked.connect(self.onSavePath) self.openPath = QtGui.QPushButton() self.openPath.setText("Open folder") self.openPath.setMinimumHeight(40) self.openPath.setObjectName("open_path") self.openPath.setToolTip("Opens the directory written to the left. By default it opens the folder where the FreeCAD project is placed") self.openPath.clicked.connect(self.onOpenPath) self.convertStlButton = QtGui.QPushButton() self.convertStlButton.setText("Convert") self.convertStlButton.setMinimumHeight(40) self.convertStlButton.setObjectName("create") self.convertStlButton.setToolTip("Create a new mesh from selected objects inside the FreeCAD project.") self.convertStlButton.clicked.connect(self.onConvertStl) self.saveStl = QtGui.QPushButton() self.saveStl.setText("Save to file") self.saveStl.setMinimumHeight(40) self.saveStl.setObjectName("save") self.saveStl.setToolTip("Export selected objects into a mesh file (STL or OBJ).") self.saveStl.clicked.connect(self.onSaveMesh) self.help = QtGui.QPushButton() infoSVG = QtGui.QIcon(':/icons/info.svg') if not infoSVG.isNull(): self.help.setIcon(infoSVG) else: self.help.setIcon(QtGui.QIcon(QtGui.QPixmap(infoIcon))) self.help.setFixedWidth(40) self.help.setObjectName("help") self.help.setToolTip("More information aboout the macro.") self.help.clicked.connect(self.onInfo) self.noSelection = QtGui.QPushButton() self.noSelection.setText("No selection") self.noSelection.setObjectName("noSelection") self.noSelection.clicked.connect(self.onError) self.infoLabel = QtGui.QLabel("") self.infoLabel.setAlignment(QtCore.Qt.AlignCenter | QtCore.Qt.AlignVCenter) spacer1 = QtGui.QSpacerItem(20, 20, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) spacer2 = QtGui.QSpacerItem(20, 20, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) spacer3 = QtGui.QSpacerItem(20, 20, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) line_separator1 = QtGui.QFrame() line_separator1.setObjectName("separator") line_separator1.setFrameShape(QtGui.QFrame.HLine) line_separator1.setFrameShadow(QtGui.QFrame.Sunken) line_separator1.setStyleSheet("background-color: rgba(0, 0, 0, 0.2);border: 1px solid transparent;border-bottom-color: rgba(255, 255, 255, 0.2);") line_separator2 = QtGui.QFrame() line_separator2.setObjectName("separator") line_separator2.setFrameShape(QtGui.QFrame.HLine) line_separator2.setFrameShadow(QtGui.QFrame.Sunken) line_separator2.setStyleSheet("background-color: rgba(0, 0, 0, 0.2);border: 1px solid transparent;border-bottom-color: rgba(255, 255, 255, 0.2);") info = QtGui.QLabel("v" + __Version__) info.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) # info.setStyleSheet("color:#2e2d2d; font-style:italic;") line = QtGui.QLabel("------------------------------------------") # addWidget(*Widget, row, column, rowspan, colspan) # Presets management self.grid.addWidget(iN9, 1, 0, 1, 1) self.grid.addWidget(self.configDropdown, 1, 1, 1, 9) self.grid.addWidget(self.deleteConfigButton, 1, 10, 1, 1) self.grid.addWidget(self.savePresetButton, 1, 11, 1, 1) self.grid.addItem(spacer1, 2, 0, 1, 12) self.grid.addWidget(line_separator1, 3, 0, 1, 12) # Convert self.grid.addWidget(iN1, 4, 0, 1, 3) self.grid.addWidget(self.d1, 4, 3, 1, 6) self.grid.addWidget(iN3, 4, 9, 1, 2) self.grid.addWidget(self.d3, 4, 11, 1, 1) self.grid.addWidget(iN2, 5, 0, 1, 3) self.grid.addWidget(self.d2, 5, 3, 1, 6) self.grid.addWidget(iN7, 6, 0, 1, 3) self.grid.addWidget(self.d7a, 6, 3, 1, 3) self.grid.addWidget(self.d7b, 7, 3, 1, 3) self.grid.addWidget(self.d7c, 8, 3, 1, 3) self.grid.addWidget(self.d4, 8, 6, 1, 6) self.grid.addWidget(iN8, 9, 0, 1, 3) self.grid.addWidget(self.d8, 9, 3, 1, 1) self.grid.addWidget(self.convertStlButton, 10, 0, 1, 12) self.grid.addItem(spacer2, 11, 0, 1, 12) self.grid.addWidget(line_separator2, 12, 0, 1, 12) # Save to file self.grid.addWidget(iN5, 13, 0, 1, 3) self.grid.addWidget(self.d5, 13, 3, 1, 9) self.grid.addWidget(iN6, 14, 0, 1, 3) self.grid.addWidget(self.d6, 14, 3, 1, 8) self.grid.addWidget(self.SavePath, 14, 11, 1, 1) self.grid.addWidget(self.saveStl, 15, 0, 1, 8) self.grid.addWidget(self.openPath, 15, 8, 1, 4) self.grid.addItem(spacer3, 16, 0, 1, 12) # Info footer self.grid.addWidget(self.help, 17, 0, 1, 1) self.grid.addWidget(self.infoLabel, 17, 1, 1, 10) self.grid.addWidget(info, 17, 11, 1, 1) # Fixed label widths iN1.setFixedWidth(140) self.setWidget(self.dockWidgetContents) self.dockWidgetContents.setContentsMargins(0, 0, 0, 0) # Load the active configuration self.loadActiveConfiguration() def updateConfigDropdown(self): self.configDropdown.clear() if dev_mode: print("") print("UPDATE START") for groupName, configName in self.configurations: self.configDropdown.addItem(configName) # Establecer el índice seleccionado según la configuración activa activeConfigIndex = self.paramGet.GetInt("ActiveConfig") if dev_mode: if dev_mode: print(" UPDATE - ActiveConfig loaded: " + str(activeConfigIndex)) # print("len(self.configurations): " + str(len(self.configurations))) if 1 <= activeConfigIndex <= len(self.configurations): if dev_mode: print(" UPDATE - Update ActiveConfig: " + str(activeConfigIndex)) self.configDropdown.setCurrentIndex(activeConfigIndex) else: if dev_mode: print(" UPDATE - Reset ActiveConfig to defaults (1)") self.configDropdown.setCurrentIndex(1) if dev_mode: print("UPDATE END") def onConfigSelected(self, index): if index >= 0 and index < len(self.configurations): groupName, configName = self.configurations[index] self.paramGet.SetInt("ActiveConfig", index + 1) # Actualiza ActiveConfig self.loadConfiguration(groupName) if dev_mode: print("") print("SAVE CONFIG - ActiveConfig: " + str(index + 1)) def loadActiveConfiguration(self): activeConfigIndex = self.paramGet.GetInt("ActiveConfig") if 1 <= activeConfigIndex <= len(self.configurations): if dev_mode: print("") print ("LOAD ACTIVE - ActiveConfig: " + str(activeConfigIndex)) # Bloquear señales para evitar llamadas redundantes a onConfigSelected self.configDropdown.blockSignals(True) self.configDropdown.setCurrentIndex(activeConfigIndex -1) self.configDropdown.blockSignals(False) # Cargar la configuración correspondiente groupName, configName = self.configurations[activeConfigIndex - 1] self.loadConfiguration(groupName) else: # Si el índice no es válido, cargar la configuración por defecto self.configDropdown.blockSignals(True) self.configDropdown.setCurrentIndex(0) self.configDropdown.blockSignals(False) groupName, configName = self.configurations[0] self.loadConfiguration(groupName) def loadConfiguration(self, groupName): configsGroup = self.paramGet.GetGroup("Configurations") configGroup = configsGroup.GetGroup(groupName) # Set the parameters self.d1.setValue(configGroup.GetFloat("SurfaceDeviation", 0.10)) self.d2.setValue(configGroup.GetFloat("AngularDeviation", 5.0)) self.d3.setChecked(configGroup.GetBool("Relative", False)) fileNameOption = configGroup.GetString("FileNameOption", "Label") if fileNameOption == "Label": self.d7a.setChecked(True) elif fileNameOption == "Project": self.d7b.setChecked(True) else: self.d7c.setChecked(True) self.d8.setChecked(configGroup.GetBool("LocalCoordinates", True)) fileFormat = configGroup.GetString("FileFormat", "Stereolithography (.STL)") index = self.d5.findText(fileFormat) if index != -1: self.d5.setCurrentIndex(index) self.d6.setText(configGroup.GetString("FileSavingDirectory", "./")) def onSaveConfig(self, configName): if not configName: self.onError("<h2>Empty Preset Name</h2><p style='font-weight:normal;'>Please enter a name for the preset.</p>") return # Determine the next available configuration number configsGroup = self.paramGet.GetGroup("Configurations") existingConfigs = configsGroup.GetGroups() # existingConfigs is a list of group names (strings) nextConfigNumber = 1 while "config_" + str(nextConfigNumber) in existingConfigs: nextConfigNumber += 1 # Save the current parameters into the new configuration newConfigGroup = configsGroup.GetGroup("config_" + str(nextConfigNumber)) newConfigGroup.SetString("ConfigName", configName) newConfigGroup.SetString("Name", configName) newConfigGroup.SetFloat("SurfaceDeviation", self.d1.value()) newConfigGroup.SetFloat("AngularDeviation", self.d2.value()) newConfigGroup.SetBool("Relative", self.d3.isChecked()) if self.d7a.isChecked(): newConfigGroup.SetString("FileNameOption", "Label") elif self.d7b.isChecked(): newConfigGroup.SetString("FileNameOption", "Project") else: newConfigGroup.SetString("FileNameOption", "Custom") newConfigGroup.SetBool("LocalCoordinates", self.d8.isChecked()) newConfigGroup.SetString("FileFormat", self.d5.currentText()) newConfigGroup.SetString("FileSavingDirectory", self.d6.text()) # Update the list of configurations self.loadConfigurations() self.updateConfigDropdown() # Set the new configuration as active self.paramGet.SetInt("ActiveConfig", len(self.configurations)) self.configDropdown.blockSignals(True) self.configDropdown.setCurrentIndex(len(self.configurations) - 1) self.configDropdown.blockSignals(False) groupName, configName = self.configurations[len(self.configurations) - 1] self.loadConfiguration(groupName) def onDeleteConfig(self): index = self.configDropdown.currentIndex() if index >= 0 and index < len(self.configurations): groupName, configName = self.configurations[index] if groupName == "config_1": self.onError("<h2>Default preset can't be deleted</h2><p style='font-weight:normal;'>Select any other preset to be deleted.</p>") return # Confirmar eliminación reply = QtGui.QMessageBox.question(None, 'Confirm preset removal', "<p style='font-weight:normal;'>Are you sure do you want to delete <span style='font-weight:bold'>" + str(configName) + "</span>?</p>", QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, QtGui.QMessageBox.No) if reply == QtGui.QMessageBox.Yes: # Eliminar la configuración configsGroup = self.paramGet.GetGroup("Configurations") configsGroup.RemGroup(groupName) # Actualizar la lista de configuraciones self.loadConfigurations() self.updateConfigDropdown() # Establecer la configuración por defecto como activa self.paramGet.SetInt("ActiveConfig", 1) self.configDropdown.blockSignals(True) self.configDropdown.setCurrentIndex(0) self.configDropdown.blockSignals(False) groupName, configName = self.configurations[0] self.loadConfiguration(groupName) self.infoMessages("Preset removed!","positive") def openSavePresetDialog(self): dialog = SavePresetDialog(self) if dialog.exec_() == QtGui.QDialog.Accepted: presetName = dialog.getName() if presetName: self.onSaveConfig(presetName) self.infoMessages("Preset saved!","positive") else: self.onError("<h2>Empty Configuration Name</h2><p style='font-weight:normal;'>Please enter a name for the configuration.</p>") def onInfo(self): msg="""<h2>Batch export to mesh</h2><p style='font-weight:normal;font-style:italic;'>version """ + __Version__ + """</p><p style='font-weight:normal;'>This macro helps Bath converting to mesh and exporting STL and OBJ files. It adds a GUI for speeding up the conversion and file saving of the selected objects</p><p>Features</p><ul><li style='font-weight:normal;'>FreeCAD "standard" meshing option</li><li style='font-weight:normal;'>custom names</li><li style='font-weight:normal;'>Converts/exports one or several objects at once</li><li style='font-weight:normal;'>Convert objects into the active FreeCAD project, a user selected folder or</li><li style='font-weight:normal;'>Export objects to .STL or .OBJ files</li><li style='font-weight:normal;'>Absolute or relative paths allowed</li><li style='font-weight:normal;'>Select local or global coordinates</li><li style='font-weight:normal;'>Creates folders if needed</li><li style='font-weight:normal;'>Opens folder in file explorer</li><li style='font-weight:normal;'>Presets loading/saving</li></ul><hr><h2>Licence</h2><p style='font-weight:normal;'>Copyright 2024 Pablo Gil Fernández</p><p style='font-weight:normal;'>This work is licensed under GNU Lesser General Public License (LGPL). To view a copy of this license, visit:</p><p style='font-weight:normal;'><a href='https://www.gnu.org/licenses/lgpl-3.0.html'>https://www.gnu.org/licenses/lgpl-3.0.html</a></p>""" # Create a message box res = QtGui.QMessageBox() res.setWindowTitle("Information") res.setText(msg) infoSVG = QtGui.QPixmap(":/icons/info.svg") if not infoSVG.isNull(): res.setIconPixmap(infoSVG) else: res.setIcon(QtGui.QMessageBox.Information) # Set a predefined icon: NoIcon, Information, Warning, Critical, Question # Show the message box res.exec_() # ERROR def onError(self, msg): res = QtGui.QMessageBox.warning(None,"Error",msg,QtGui.QMessageBox.Ok) # GET SAVING PATH def onSavePath(self): folder_path = QtGui.QFileDialog.getExistingDirectory() if folder_path: if dev_mode: self.message(folder_path) self.d6.setText(folder_path) # OPEN PATH IN FILE BROWSER def onOpenPath(self): if dev_mode: print("\nOpen directory\n===================") project_filename_path = FreeCAD.ActiveDocument.FileName project_path = os.path.dirname(project_filename_path) length_project_path = len(project_path) final_folder_path = "" user_path = self.d6.text() length_user_path = len(user_path) if dev_mode: print("\nproject_path = " + project_path) print("\nlength_project_path = " + str(length_project_path)) print("\nuser_path = " + user_path) print("\nlength_user_path = " + str(length_user_path)) if length_project_path == 0: FreeCAD.Console.PrintError("\nProject not saved!\nIt is not possible to open the file browser. Try again after saving your project somewhere.") self.onError("<h2>Project not saved!</h2><p style='font-weight:normal;'>It is not possible to open the file browser. Try again after saving your project somewhere.</p>") else: if length_user_path == 0: if dev_mode: self.message("\n-- Absolute mode (user path) --") final_folder_path = project_path else: project_filename_path = FreeCAD.ActiveDocument.FileName project_path = os.path.dirname(project_filename_path) length_user_path = len(user_path) if length_user_path == 0: if dev_mode: self.message("\n-- Absolute mode (project path) --") final_folder_path = project_path else: if dev_mode: self.message("\n-- Relative mode --") joined_path = os.path.join(project_path, user_path) final_folder_path = os.path.abspath(joined_path) if os.path.exists(final_folder_path): if dev_mode: self.message("\nThe path exists") self.message("\nfinal_folder_path: " + final_folder_path) if dev_mode: print("\nOpening...") FreeCAD.Console.PrintMessage("\nOpening...") operating_system = sys.platform if operating_system == "darwin": if dev_mode: print("\nOpening macOS Finder...") subprocess.run(["open", final_folder_path]) elif operating_system == "win32": if dev_mode: print("\nOpening File Explorer...") subprocess.run(["explorer", final_folder_path], shell=True) elif operating_system == "linux": if dev_mode: print("\nOpening Linux file explorer...") subprocess.run(["xdg-open", final_folder_path]) else: FreeCAD.Console.PrintError("\nUnsupported operating system") self.onError("<h2>Unsupported operating system</h2><p style='font-weight:normal;'>It is not possible to open your OS file browser.</p>") else: FreeCAD.Console.PrintError("\nThe path doesn't exist, please create it") self.onError("<h2>The path doesn't exist</h2><p style='font-weight:normal;'>Please create it manually.</p>") # CONVERT STL INTO ACTIVE FREECAD PROJECT def onConvertStl(self): if dev_mode: self.clear_console() self.message("\n****** START (convert) ********") # Variables selection = Gui.Selection.getSelection() surface = self.d1.value() angular = self.d2.value() * math.pi / 180 if self.d3.isChecked(): relative = True else: relative = False name = self.d4.text() if len(selection) == 0: if dev_mode: self.message("\n-- No selection --") FreeCAD.Console.PrintError("Export STL error: nothing selected, please, select one or several objects") self.onError("<h2>Nothing selected</h2><p style='font-weight:normal;'>You must select at least one object to convert or export.</p>") elif len(selection) == 1: if dev_mode: self.message("\n-- 1 object selected --") # Generate Mesh mesh = App.ActiveDocument.addObject("Mesh::Feature","Mesh") part = Gui.Selection.getSelection()[0] shape = part.Shape.copy(False) # Set part coordinates (local or global) if self.d8.isChecked(): shape.Placement = App.Placement(App.Vector(0,0,0), App.Rotation(App.Vector(0,0,1),0), App.Vector(0,0,0)) else: shape.Placement = part.getGlobalPlacement() # Create mesh mesh.Mesh = MeshPart.meshFromShape(Shape=shape, LinearDeflection=surface, AngularDeflection=angular, Relative=relative) # Name mesh if self.d7a.isChecked(): final_name = Gui.Selection.getSelection()[0].Label elif self.d7b.isChecked(): final_name = App.ActiveDocument.Name else: final_name = name mesh.Label = final_name part.Visibility = False Gui.activeDocument().activeObject().DisplayMode=u"Flat Lines" if dev_mode: self.message("\n-- 1 object converted to mesh --") del mesh, part, shape self.infoMessages("1 object converted to mesh","positive") else: quantity = len(selection) if dev_mode: self.message("\n-- " + str(quantity) + " objects selected --") i = 0 while i < quantity: mesh = App.ActiveDocument.addObject("Mesh::Feature","Mesh") part = Gui.Selection.getSelection()[i] shape = part.Shape.copy(False) # Set part coordinates (local or global) if self.d8.isChecked(): shape.Placement = App.Placement(App.Vector(0,0,0), App.Rotation(App.Vector(0,0,1),0), App.Vector(0,0,0)) else: shape.Placement = part.getGlobalPlacement() # Create mesh mesh.Mesh = MeshPart.meshFromShape(Shape=shape, LinearDeflection=surface, AngularDeflection=angular, Relative=relative) # Name mesh if self.d7a.isChecked(): mesh.Label = Gui.Selection.getSelection()[i].Label elif self.d7b.isChecked(): mesh.Label = App.ActiveDocument.Name + "_" + str(i+1) else: mesh.Label = name + "_" + str(i+1) part.Visibility = False Gui.activeDocument().activeObject().DisplayMode=u"Flat Lines" del mesh, part, shape i += 1 if dev_mode: self.message("\n-- " + str(quantity) + " objects converted to mesh --") self.infoMessages(str(quantity) + " objects converted to mesh","positive") App.ActiveDocument.recompute() if dev_mode: self.message("\n********* END *********") FreeCAD.Console.PrintMessage("\nConversion done") # SAVE TO .STL OR .OBJ FILE def onSaveMesh(self): if dev_mode: self.clear_console() self.message("\n****** START (save) ********") # Variables selection = Gui.Selection.getSelection() surface = self.d1.value() angular = self.d2.value() * math.pi / 180 if self.d3.isChecked(): relative = True else: relative = False name = self.d4.text() savePath = self.d6.text() docPath = FreeCAD.ActiveDocument.FileName if docPath == "": if savePath[0] == ".": if dev_mode: self.message("\n-- The project is not saved or the path doesn't correspond to an absolute one --") FreeCAD.Console.PrintError("Error exporting: The project is not saved or the path doesn't correspond to an absolute one, please, save your project somewhere or add a valid absolute path under '<i>File saving directory</i>' field.") self.onError("<h2>Error exporting</h2><p style='font-weight:normal;'>The project is not saved or the path doesn't correspond to an absolute one, please, save your project somewhere or add a valid absolute path under '<i>File saving directory</i>' field.</p>") return if os.path.exists(savePath) and os.path.isdir(savePath): if dev_mode: self.message("\nsavePath is valid, continuing... ") else: if dev_mode: self.message("\n-- The project is not saved or the path doesn't correspond to an absolute one --") FreeCAD.Console.PrintError("Error exporting: The project is not saved or the path doesn't correspond to an absolute one, please, save your project somewhere or add a valid absolute path under '<i>File saving directory</i>' field.") self.onError("<h2>Error exporting</h2><p style='font-weight:normal;'>The project is not saved or the path doesn't correspond to an absolute one, please, save your project somewhere or add a valid absolute path under '<i>File saving directory</i>' field.</p>") return if savePath[0] == ".": if dev_mode: self.message("\n-- Relative mode: --") absolutePath = os.path.dirname(docPath) relativePath = savePath.replace(".","",1) relativePath = relativePath.replace("\\","",1) relativePath = relativePath.replace("/","",1) savePath = os.path.join(absolutePath, relativePath) if len(selection) == 0: if dev_mode: self.message("\n-- No selection --") FreeCAD.Console.PrintError("Export STL error: nothing selected, please, select one or several objects") self.onError("<h2>Nothing selected</h2><p style='font-weight:normal;'>You must select at least one object to convert or export.</p>") return elif len(selection) == 1: if dev_mode: self.message("\n-- 1 object selected --") # Generate STL obj=[] part = Gui.Selection.getSelection()[0] shape = part.Shape.copy(False) # Set part coordinates (local or global) if self.d8.isChecked(): shape.Placement = App.Placement(App.Vector(0,0,0), App.Rotation(App.Vector(0,0,1),0), App.Vector(0,0,0)) else: shape.Placement = part.getGlobalPlacement() # Create mesh mesh = MeshPart.meshFromShape(Shape=shape, LinearDeflection=surface, AngularDeflection=angular, Relative=relative) # Name mesh if self.d7a.isChecked(): final_name = Gui.Selection.getSelection()[0].Label elif self.d7b.isChecked(): final_name = App.ActiveDocument.Name else: final_name = name # Check if the path exists: if not os.path.exists(savePath): if dev_mode: FreeCAD.Console.PrintWarning("\nThe path doesn't exist, creating it...") os.makedirs(savePath, exist_ok=True) else: if dev_mode: print("\nThe path exist, continuing...") if self.d5.currentText() == "Stereolithography (.STL)": # export mesh to STL filename = os.path.join(savePath, final_name + ".stl") mesh.write(filename) if self.d5.currentText() == "Alias mesh (.OBJ)": # export mesh to OBJ filename = os.path.join(savePath, final_name + ".obj") obj = [] obj.append(part) Mesh.export(obj, filename) if dev_mode: self.message("\n-- 1 mesh exported to file at: " + filename + " --") del mesh, obj, part, shape if dev_mode: FreeCAD.Console.PrintMessage("\nSaved") self.infoMessages("1 object exported to file","positive") else: quantity = len(selection) if dev_mode: self.message("\n-- " + str(quantity) + " objects selected --") i = 0 namesArray = [] labelsArray = [] while i < quantity: obj=[] part = Gui.Selection.getSelection()[i] shape = part.Shape.copy(False) # Set part coordinates (local or global) if self.d8.isChecked(): shape.Placement = App.Placement(App.Vector(0,0,0), App.Rotation(App.Vector(0,0,1),0), App.Vector(0,0,0)) else: shape.Placement = part.getGlobalPlacement() # Create mesh mesh = MeshPart.meshFromShape(Shape=shape, LinearDeflection=surface, AngularDeflection=angular, Relative=relative) # Name mesh if self.d7a.isChecked(): final_name = Gui.Selection.getSelection()[i].Label elif self.d7b.isChecked(): final_name = App.ActiveDocument.Name + "_" + str(i+1) else: final_name = name + "_" + str(i+1) labelsArray.append(final_name) # Check if the path exists: if not os.path.exists(savePath): FreeCAD.Console.PrintWarning("\nThe path doesn't exist, creating it...") os.makedirs(savePath, exist_ok=True) else: if dev_mode: print("\nThe path exist, continuing...") if self.d5.currentText() == "Stereolithography (.STL)": # export mesh to STL filename = os.path.join(savePath, labelsArray[i] + ".stl") mesh.write(filename) if self.d5.currentText() == "Alias mesh (.OBJ)": # export mesh to OBJ filename = os.path.join(savePath, labelsArray[i] + ".obj") obj = [] obj.append(part) Mesh.export(obj, filename) if dev_mode: self.message("\n-- 1 mesh exported to file at: " + filename + " --") del mesh, obj, part, shape i += 1 i = 0 while i < quantity: if dev_mode: self.message("\n-- " + str(labelsArray[i]) + " exported --") i += 1 if dev_mode: FreeCAD.Console.PrintMessage("\nSaved") self.infoMessages(str(quantity) + " objects exported to file","positive") App.ActiveDocument.recompute() if dev_mode: self.message("\n********* END *********") def clear_console(self): #clearing previous messages mw=FreeCADGui.getMainWindow() r=mw.findChild(QtGui.QTextEdit, "Report view") r.clear() def message(self,msg): FreeCAD.Console.PrintMessage("BatchETM: " + str(msg) + "\n") def message_warning(self,msg): FreeCAD.Console.PrintWarning("BatchETM: " + str(msg) + "\n") def message_error(self,msg): FreeCAD.Console.PrintError("BatchETM: " + str(msg) + "\n") def infoMessages(self,msg,type): if type == "positive": self.infoLabel.setStyleSheet("color: rgb(19, 27, 2);background-color: rgba(183, 192, 165, 0.5);border-radius: 8px;") # positive elif type == "negative": self.infoLabel.setStyleSheet("color: rgb(20, 0, 0);background-color: rgba(172, 40, 40, 0.5);border-radius: 8px;") # negative elif type == "reset": self.infoLabel.setStyleSheet("color: black;background-color: rgba(0, 0, 0, 0);border-radius: 8px;") # reset else: self.infoLabel.setStyleSheet("color: rgb(0, 0, 255);background-color: rgba(183, 192, 165, 0.5);border-radius: 8px;") self.infoLabel.setText(str(msg)) BatchETM = BatchExportToMesh() BatchETM.show() BatchETM.destroyed.connect(lambda: FreeCADGui.getMainWindow().findChild(QtGui.QWidget, "BatchETM").deleteLater())
Copyright 2024 Pablo Gil Fernández
This work is licensed under GNU Lesser General Public License (LGPL). To view a copy of this license, visit:
https://www.gnu.org/licenses/lgpl-3.0.html