# -*- coding: utf-8 -*- # *************************************************************************** # * Copyright (c) 2019 sliptonic * # * Copyright (c) 2020 Schildkroet * # * * # * 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 * # * * # *************************************************************************** import FreeCAD import FreeCADGui import Path import Path.Tool.Bit as PathToolBit import Path.Tool.Gui.Bit as PathToolBitGui import Path.Tool.Gui.BitEdit as PathToolBitEdit import Path.Tool.Gui.Controller as PathToolControllerGui import PathGui import PathScripts.PathUtilsGui as PathUtilsGui import PySide import glob import json import os import shutil import uuid as UUID from functools import partial from PySide.QtGui import QStandardItem, QStandardItemModel, QPixmap from PySide.QtCore import Qt if False: Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) Path.Log.trackModule(Path.Log.thisModule()) else: Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) _UuidRole = PySide.QtCore.Qt.UserRole + 1 _PathRole = PySide.QtCore.Qt.UserRole + 2 translate = FreeCAD.Qt.translate def checkWorkingDir(): """Check the tool library directory writable and configure a new library if required""" # users shouldn't use the example toolbits and libraries. # working directory should be writable Path.Log.track() workingdir = os.path.dirname(Path.Preferences.lastPathToolLibrary()) defaultdir = os.path.dirname(Path.Preferences.pathDefaultToolsPath()) Path.Log.debug("workingdir: {} defaultdir: {}".format(workingdir, defaultdir)) dirOK = lambda: workingdir != defaultdir and (os.access(workingdir, os.W_OK)) if dirOK(): return True qm = PySide.QtGui.QMessageBox ret = qm.question( None, "", translate("CAM_ToolBit", "Toolbit working directory not set up. Do that now?"), qm.Yes | qm.No, ) if ret == qm.No: return False msg = translate("CAM_ToolBit", "Choose a writable location for your toolbits") while not dirOK(): workingdir = PySide.QtGui.QFileDialog.getExistingDirectory( None, msg, Path.Preferences.filePath() ) if workingdir[-8:] == os.path.sep + "Library": workingdir = workingdir[:-8] # trim off trailing /Library if user chose it Path.Preferences.setLastPathToolLibrary("{}{}Library".format(workingdir, os.path.sep)) Path.Preferences.setLastPathToolBit("{}{}Bit".format(workingdir, os.path.sep)) Path.Log.debug("setting workingdir to: {}".format(workingdir)) # Copy only files of default Path/Tool folder to working directory # (targeting the README.md help file) src_toolfiles = os.listdir(defaultdir) for file_name in src_toolfiles: if file_name in ["README.md"]: full_file_name = os.path.join(defaultdir, file_name) if os.path.isfile(full_file_name): shutil.copy(full_file_name, workingdir) # Determine which subdirectories are missing subdirlist = ["Bit", "Library", "Shape"] mode = 0o777 for dir in subdirlist.copy(): subdir = "{}{}{}".format(workingdir, os.path.sep, dir) if os.path.exists(subdir): subdirlist.remove(dir) # Query user for creation permission of any missing subdirectories if len(subdirlist) >= 1: needed = ", ".join([str(d) for d in subdirlist]) qm = PySide.QtGui.QMessageBox ret = qm.question( None, "", translate( "CAM_ToolBit", "Toolbit Working directory {} needs these sudirectories:\n {} \n Create them?", ).format(workingdir, needed), qm.Yes | qm.No, ) if ret == qm.No: return False else: # Create missing subdirectories if user agrees to creation for dir in subdirlist: subdir = "{}{}{}".format(workingdir, os.path.sep, dir) os.mkdir(subdir, mode) # Query user to copy example files into subdirectories created if dir != "Shape": qm = PySide.QtGui.QMessageBox ret = qm.question( None, "", translate("CAM_ToolBit", "Copy example files to new {} directory?").format( dir ), qm.Yes | qm.No, ) if ret == qm.Yes: src = "{}{}{}".format(defaultdir, os.path.sep, dir) src_files = os.listdir(src) for file_name in src_files: full_file_name = os.path.join(src, file_name) if os.path.isfile(full_file_name): shutil.copy(full_file_name, subdir) # if no library is set, choose the first one in the Library directory if Path.Preferences.lastFileToolLibrary() is None: libFiles = [ f for f in glob.glob(Path.Preferences.lastPathToolLibrary() + os.path.sep + "*.fctl") ] Path.Preferences.setLastFileToolLibrary(libFiles[0]) return True class _TableView(PySide.QtGui.QTableView): """Subclass of QTableView to support rearrange and copying of ToolBits""" def __init__(self, parent): PySide.QtGui.QTableView.__init__(self, parent) self.setDragEnabled(False) self.setAcceptDrops(False) self.setDropIndicatorShown(False) self.setDragDropMode(PySide.QtGui.QAbstractItemView.DragOnly) self.setDefaultDropAction(PySide.QtCore.Qt.IgnoreAction) self.setSortingEnabled(True) self.setSelectionBehavior(PySide.QtGui.QAbstractItemView.SelectRows) self.verticalHeader().hide() def supportedDropActions(self): return [PySide.QtCore.Qt.CopyAction, PySide.QtCore.Qt.MoveAction] def _uuidOfRow(self, row): model = self.toolModel() return model.data(model.index(row, 0), _UuidRole) def _rowWithUuid(self, uuid): model = self.toolModel() for row in range(model.rowCount()): if self._uuidOfRow(row) == uuid: return row return None def _copyTool(self, uuid_, dstRow): model = self.toolModel() model.insertRow(dstRow) srcRow = self._rowWithUuid(uuid_) for col in range(model.columnCount()): srcItem = model.item(srcRow, col) model.setData( model.index(dstRow, col), srcItem.data(PySide.QtCore.Qt.EditRole), PySide.QtCore.Qt.EditRole, ) if col == 0: model.setData(model.index(dstRow, col), srcItem.data(_PathRole), _PathRole) # Even a clone of a tool gets its own uuid so it can be identified when # rearranging the order or inserting/deleting rows model.setData(model.index(dstRow, col), UUID.uuid4(), _UuidRole) else: model.item(dstRow, col).setEditable(False) def _copyTools(self, uuids, dst): for i, uuid in enumerate(uuids): self._copyTool(uuid, dst + i) def dropEvent(self, event): """Handle drop events on the tool table""" Path.Log.track() mime = event.mimeData() data = mime.data("application/x-qstandarditemmodeldatalist") stream = PySide.QtCore.QDataStream(data) srcRows = [] while not stream.atEnd(): row = stream.readInt32() srcRows.append(row) # get the uuids of all srcRows model = self.toolModel() srcUuids = [self._uuidOfRow(row) for row in set(srcRows)] destRow = self.rowAt(event.pos().y()) self._copyTools(srcUuids, destRow) if PySide.QtCore.Qt.DropAction.MoveAction == event.proposedAction(): for uuid in srcUuids: model.removeRow(self._rowWithUuid(uuid)) class ModelFactory: """Helper class to generate qtdata models for toolbit libraries""" @staticmethod def find_libraries(model) -> QStandardItemModel: """ Finds all the fctl files in a location. Returns a QStandardItemModel. """ Path.Log.track() path = Path.Preferences.lastPathToolLibrary() model.clear() if os.path.isdir(path): # opening all tables in a directory libFiles = [f for f in glob.glob(path + os.path.sep + "*.fctl")] libFiles.sort() for libFile in libFiles: loc, fnlong = os.path.split(libFile) fn, ext = os.path.splitext(fnlong) libItem = QStandardItem(fn) libItem.setToolTip(loc) libItem.setData(libFile, _PathRole) libItem.setIcon(QPixmap(":/icons/CAM_ToolTable.svg")) model.appendRow(libItem) Path.Log.debug("model rows: {}".format(model.rowCount())) return model @staticmethod def __library_load(path: str, data_model: QStandardItemModel): Path.Log.track(path) Path.Preferences.setLastFileToolLibrary(path) try: with open(path) as fp: library = json.load(fp) except Exception as e: Path.Log.error(f"Failed to load library from {path}: {e}") return for tool_bit in library.get("tools", []): try: nr = tool_bit["nr"] bit = PathToolBit.findToolBit(tool_bit["path"], path) if bit: Path.Log.track(bit) tool = PathToolBit.Declaration(bit) data_model.appendRow(ModelFactory._tool_add(nr, tool, bit)) else: Path.Log.error(f"Could not find tool #{nr}: {tool_bit['path']}") except Exception as e: msg = f"Error loading tool: {tool_bit['path']} : {e}" FreeCAD.Console.PrintError(msg) @staticmethod def _generate_tooltip(toolbit: dict) -> str: """ Generate an HTML tooltip for a given toolbit dictionary. Args: toolbit (dict): A dictionary containing toolbit information. Returns: str: An HTML string representing the tooltip. """ tooltip = f"Name: {toolbit['name']}
" tooltip += f"Shape File: {toolbit['shape']}
" tooltip += "Parameters:
" parameters = toolbit.get("parameter", {}) if parameters: for key, value in parameters.items(): tooltip += f" {key}: {value}
" else: tooltip += " No parameters provided.
" attributes = toolbit.get("attribute", {}) if attributes: tooltip += "Attributes:
" for key, value in attributes.items(): tooltip += f" {key}: {value}
" return tooltip @staticmethod def _tool_add(nr: int, tool: dict, path: str): str_shape = os.path.splitext(os.path.basename(tool["shape"]))[0] tooltip = ModelFactory._generate_tooltip(tool) tool_nr = QStandardItem() tool_nr.setData(nr, Qt.EditRole) tool_nr.setData(path, _PathRole) tool_nr.setData(UUID.uuid4(), _UuidRole) tool_nr.setToolTip(tooltip) tool_name = QStandardItem() tool_name.setData(tool["name"], Qt.EditRole) tool_name.setEditable(False) tool_name.setToolTip(tooltip) tool_shape = QStandardItem() tool_shape.setData(str_shape, Qt.EditRole) tool_shape.setEditable(False) return [tool_nr, tool_name, tool_shape] @staticmethod def new_tool(datamodel: QStandardItemModel, path: str): """ Adds a toolbit item to a model. """ Path.Log.track() try: nr = ( max( ( int(datamodel.item(row, 0).data(Qt.EditRole)) for row in range(datamodel.rowCount()) ), default=0, ) + 1 ) tool = PathToolBit.Declaration(path) except Exception as e: Path.Log.error(e) return datamodel.appendRow(ModelFactory._tool_add(nr, tool, path)) @staticmethod def library_open(model: QStandardItemModel, lib: str = "") -> QStandardItemModel: """ Opens the tools in a library. Returns a QStandardItemModel. """ Path.Log.track(lib) if not lib: lib = Path.Preferences.lastFileToolLibrary() if not lib or not os.path.isfile(lib): return model ModelFactory.__library_load(lib, model) Path.Log.debug("model rows: {}".format(model.rowCount())) return model class ToolBitSelector(object): """Controller for displaying a library and creating ToolControllers""" def __init__(self): checkWorkingDir() self.form = FreeCADGui.PySideUic.loadUi(":/panels/ToolBitSelector.ui") self.factory = ModelFactory() self.toolModel = PySide.QtGui.QStandardItemModel(0, len(self.columnNames())) self.libraryModel = PySide.QtGui.QStandardItemModel(0, len(self.columnNames())) self.setupUI() self.title = self.form.windowTitle() def columnNames(self): """Define the column names to display""" return ["#", "Tool"] def currentLibrary(self, shortNameOnly): """Get the file path for the current tool library""" libfile = Path.Preferences.lastFileToolLibrary() if libfile is None or libfile == "": return "" elif shortNameOnly: return os.path.splitext(os.path.basename(libfile))[0] return libfile def loadData(self): """Load the toolbits for the selected tool library""" Path.Log.track() self.toolModel.clear() self.toolModel.setHorizontalHeaderLabels(self.columnNames()) # Get the currently selected index in the combobox currentIndex = self.form.cboLibraries.currentIndex() if currentIndex != -1: # Get the data for the selected index libPath = self.libraryModel.item(currentIndex).data(_PathRole) self.factory.library_open(self.toolModel, libPath) self.toolModel.takeColumn(3) self.toolModel.takeColumn(2) def loadToolLibraries(self): """ Load the tool libraries in to self.libraryModel and populate the tooldock form combobox with the libraries names """ Path.Log.track() # Get the current library so we can try and maintain any previous selection current_lib = self.currentLibrary(True) # True to get short name only # load the tool libraries self.factory.find_libraries(self.libraryModel) # Set the library model to the combobox self.form.cboLibraries.setModel(self.libraryModel) # Set the current library as the selected item in the combobox currentIndex = self.form.cboLibraries.findText(current_lib) # Set the selected library as the currentIndex in the combobox if currentIndex == -1 and self.libraryModel.rowCount() > 0: # If current library is not found, default to the first item currentIndex = 0 self.form.cboLibraries.setCurrentIndex(currentIndex) def setupUI(self): """Setup the form and load the tooltable data""" Path.Log.track() # Connect the library change to reload data and update tooltip self.form.cboLibraries.currentIndexChanged.connect(self.loadData) self.form.cboLibraries.currentIndexChanged.connect(self.updateLibraryTooltip) # Load the tool libraries. # This will trigger a change in current index of the cboLibraries combobox self.loadToolLibraries() self.form.tools.setModel(self.toolModel) self.form.tools.selectionModel().selectionChanged.connect(self.enableButtons) self.form.tools.doubleClicked.connect(partial(self.selectedOrAllToolControllers)) self.form.libraryEditorOpen.clicked.connect(self.libraryEditorOpen) self.form.addToolController.clicked.connect(self.selectedOrAllToolControllers) def updateLibraryTooltip(self, index): """Add a tooltip to the combobox""" if index != -1: item = self.libraryModel.item(index) if item: libPath = item.data(_PathRole) self.form.cboLibraries.setToolTip(f"{libPath}") else: self.form.cboLibraries.setToolTip(translate("CAM_Toolbit", "Select a library")) else: self.form.cboLibraries.setToolTip(translate("CAM_Toolbit", "No library selected")) def enableButtons(self): """Enable button to add tool controller when a tool is selected""" # Set buttons inactive self.form.addToolController.setEnabled(False) selected = len(self.form.tools.selectedIndexes()) >= 1 if selected: jobs = len([1 for j in FreeCAD.ActiveDocument.Objects if j.Name[:3] == "Job"]) >= 1 self.form.addToolController.setEnabled(selected and jobs) def libraryEditorOpen(self): library = ToolBitLibrary() library.open() self.loadToolLibraries() def selectedOrAllTools(self): """ Iterate the selection and add individual tools If a group is selected, iterate and add children """ itemsToProcess = [] for index in self.form.tools.selectedIndexes(): item = index.model().itemFromIndex(index) if item.hasChildren(): for i in range(item.rowCount() - 1): if item.child(i).column() == 0: itemsToProcess.append(item.child(i)) elif item.column() == 0: itemsToProcess.append(item) tools = [] for item in itemsToProcess: toolNr = int(item.data(PySide.QtCore.Qt.EditRole)) toolPath = item.data(_PathRole) tools.append((toolNr, PathToolBit.Factory.CreateFrom(toolPath))) return tools def selectedOrAllToolControllers(self, index=None): """ if no jobs, don't do anything, otherwise all TCs for all selected toolbits """ jobs = PathUtilsGui.PathUtils.GetJobs() if len(jobs) == 0: return elif len(jobs) == 1: job = jobs[0] else: userinput = PathUtilsGui.PathUtilsUserInput() job = userinput.chooseJob(jobs) if job is None: # user may have canceled return tools = self.selectedOrAllTools() for tool in tools: tc = PathToolControllerGui.Create("TC: {}".format(tool[1].Label), tool[1], tool[0]) job.Proxy.addToolController(tc) FreeCAD.ActiveDocument.recompute() def open(self, path=None): """load library stored in path and bring up ui""" docs = FreeCADGui.getMainWindow().findChildren(PySide.QtGui.QDockWidget) for doc in docs: if doc.objectName() == "ToolSelector": if doc.isVisible(): doc.deleteLater() return else: doc.setVisible(True) return mw = FreeCADGui.getMainWindow() mw.addDockWidget( PySide.QtCore.Qt.RightDockWidgetArea, self.form, PySide.QtCore.Qt.Orientation.Vertical, ) class ToolBitLibrary(object): """ToolBitLibrary is the controller for displaying/selecting/creating/editing a collection of ToolBits.""" def __init__(self): Path.Log.track() checkWorkingDir() self.factory = ModelFactory() self.temptool = None self.toolModel = PySide.QtGui.QStandardItemModel(0, len(self.columnNames())) self.listModel = PySide.QtGui.QStandardItemModel() self.form = FreeCADGui.PySideUic.loadUi(":/panels/ToolBitLibraryEdit.ui") self.toolTableView = _TableView(self.form.toolTableGroup) self.form.toolTableGroup.layout().replaceWidget(self.form.toolTable, self.toolTableView) self.form.toolTable.hide() self.setupUI() self.title = self.form.windowTitle() def toolBitNew(self): """Create a new toolbit""" Path.Log.track() # select the shape file shapefile = PathToolBitGui.GetToolShapeFile() if shapefile is None: # user canceled return # select the bit file location and filename filename = PathToolBitGui.GetNewToolFile() if filename is None: return # Parse out the name of the file and write the structure loc, fil = os.path.split(filename) fname = os.path.splitext(fil)[0] fullpath = "{}{}{}.fctb".format(loc, os.path.sep, fname) Path.Log.debug("fullpath: {}".format(fullpath)) self.temptool = PathToolBit.ToolBitFactory().Create(name=fname) self.temptool.BitShape = shapefile self.temptool.Proxy.unloadBitBody(self.temptool) self.temptool.Label = fname self.temptool.Proxy.saveToFile(self.temptool, fullpath) self.temptool.Document.removeObject(self.temptool.Name) self.temptool = None # add it to the model self.factory.new_tool(self.toolModel, fullpath) self.librarySave() def toolBitExisting(self): """Add an existing toolbit to the library""" filenames = PathToolBitGui.GetToolFiles() if len(filenames) == 0: return for f in filenames: loc, fil = os.path.split(f) fname = os.path.splitext(fil)[0] fullpath = "{}{}{}.fctb".format(loc, os.path.sep, fname) self.factory.new_tool(self.toolModel, fullpath) self.librarySave() def toolDelete(self): """Delete a tool""" Path.Log.track() selectedRows = set([index.row() for index in self.toolTableView.selectedIndexes()]) for row in sorted(list(selectedRows), key=lambda r: -r): self.toolModel.removeRows(row, 1) self.librarySave() def toolSelect(self, selected, deselected): sel = len(self.toolTableView.selectedIndexes()) > 0 self.form.toolDelete.setEnabled(sel) def tableSelected(self, index): """loads the tools for the selected tool table""" Path.Log.track() item = index.model().itemFromIndex(index) libpath = item.data(_PathRole) self.loadData(libpath) self.path = libpath def open(self): Path.Log.track() return self.form.exec_() def libraryPath(self): """Select and load a tool library""" Path.Log.track() path = PySide.QtGui.QFileDialog.getExistingDirectory( self.form, "Tool Library Path", Path.Preferences.lastPathToolLibrary() ) if len(path) == 0: return Path.Preferences.setLastPathToolLibrary(path) self.loadData() def cleanupDocument(self): """Clean up the document""" # This feels like a hack. Remove the toolbit object # remove the editor from the dialog # re-enable all the controls self.temptool.Proxy.unloadBitBody(self.temptool) self.temptool.Document.removeObject(self.temptool.Name) self.temptool = None widget = self.form.toolTableGroup.children()[-1] widget.setParent(None) self.editor = None self.lockoff() def accept(self): """Handle accept signal""" self.editor.accept() self.temptool.Proxy.saveToFile(self.temptool, self.temptool.File) self.librarySave() self.loadData() self.cleanupDocument() def reject(self): """Handle reject signal""" self.cleanupDocument() def lockon(self): """Set the state of the form widgets: inactive""" self.toolTableView.setEnabled(False) self.form.toolCreate.setEnabled(False) self.form.toolDelete.setEnabled(False) self.form.toolAdd.setEnabled(False) self.form.TableList.setEnabled(False) self.form.libraryOpen.setEnabled(False) self.form.libraryExport.setEnabled(False) self.form.addToolTable.setEnabled(False) self.form.librarySave.setEnabled(False) def lockoff(self): """Set the state of the form widgets: active""" self.toolTableView.setEnabled(True) self.form.toolCreate.setEnabled(True) self.form.toolDelete.setEnabled(True) self.form.toolAdd.setEnabled(True) self.form.toolTable.setEnabled(True) self.form.TableList.setEnabled(True) self.form.libraryOpen.setEnabled(True) self.form.libraryExport.setEnabled(True) self.form.addToolTable.setEnabled(True) self.form.librarySave.setEnabled(True) def toolEdit(self, selected): """Edit the selected tool bit""" Path.Log.track() item = self.toolModel.item(selected.row(), 0) if self.temptool is not None: self.temptool.Document.removeObject(self.temptool.Name) if selected.column() == 0: # editing Nr pass else: tbpath = item.data(_PathRole) self.temptool = PathToolBit.ToolBitFactory().CreateFrom(tbpath, "temptool") self.editor = PathToolBitEdit.ToolBitEditor( self.temptool, self.form.toolTableGroup, loadBitBody=False ) QBtn = PySide.QtGui.QDialogButtonBox.Ok | PySide.QtGui.QDialogButtonBox.Cancel buttonBox = PySide.QtGui.QDialogButtonBox(QBtn) buttonBox.accepted.connect(self.accept) buttonBox.rejected.connect(self.reject) layout = self.editor.form.layout() layout.addWidget(buttonBox) self.lockon() self.editor.setupUI() def toolEditDone(self, success=True): FreeCAD.ActiveDocument.removeObject("temptool") print("all done") def libraryNew(self): """Create a new tool library""" TooltableTypeJSON = translate("CAM_ToolBit", "Tooltable JSON (*.fctl)") filename = PySide.QtGui.QFileDialog.getSaveFileName( self.form, translate("CAM_ToolBit", "Save toolbit library"), Path.Preferences.lastPathToolLibrary(), "{}".format(TooltableTypeJSON), ) if not (filename and filename[0]): self.loadData() path = filename[0] if filename[0].endswith(".fctl") else "{}.fctl".format(filename[0]) library = {} tools = [] library["version"] = 1 library["tools"] = tools with open(path, "w") as fp: json.dump(library, fp, sort_keys=True, indent=2) self.loadData() def librarySave(self): """Save the tool library""" library = {} tools = [] library["version"] = 1 library["tools"] = tools for row in range(self.toolModel.rowCount()): toolNr = self.toolModel.data(self.toolModel.index(row, 0), PySide.QtCore.Qt.EditRole) toolPath = self.toolModel.data(self.toolModel.index(row, 0), _PathRole) if Path.Preferences.toolsStoreAbsolutePaths(): bitPath = toolPath else: # bitPath = PathToolBit.findRelativePathTool(toolPath) # Extract the name of the shape file __, filShp = os.path.split( toolPath ) # __ is an ignored placeholder acknowledged by LGTM bitPath = str(filShp) tools.append({"nr": toolNr, "path": bitPath}) if self.path is not None: with open(self.path, "w") as fp: json.dump(library, fp, sort_keys=True, indent=2) def libraryOk(self): self.librarySave() self.form.close() def libPaths(self): """Get the file path for the last used tool library""" lib = Path.Preferences.lastFileToolLibrary() loc = Path.Preferences.lastPathToolLibrary() Path.Log.track("lib: {} loc: {}".format(lib, loc)) return lib, loc def columnNames(self): return [ "Tn", translate("CAM_ToolBit", "Tool"), translate("CAM_ToolBit", "Shape"), ] def loadData(self, path=None): """Load tooltable data""" Path.Log.track(path) self.toolTableView.setUpdatesEnabled(False) self.form.TableList.setUpdatesEnabled(False) if path is None: path, loc = self.libPaths() self.toolModel.clear() self.listModel.clear() self.factory.library_open(self.toolModel, lib=path) self.factory.find_libraries(self.listModel) else: self.toolModel.clear() self.factory.library_open(self.toolModel, lib=path) self.path = path self.form.setWindowTitle("{}".format(Path.Preferences.lastPathToolLibrary())) self.toolModel.setHorizontalHeaderLabels(self.columnNames()) self.listModel.setHorizontalHeaderLabels(["Library"]) # Select the current library in the list of tables curIndex = None for i in range(self.listModel.rowCount()): item = self.listModel.item(i) if item.data(_PathRole) == path: curIndex = self.listModel.indexFromItem(item) if curIndex: sm = self.form.TableList.selectionModel() sm.select(curIndex, PySide.QtCore.QItemSelectionModel.Select) self.toolTableView.setUpdatesEnabled(True) self.form.TableList.setUpdatesEnabled(True) def setupUI(self): """Setup the form and load the tool library data""" Path.Log.track() self.form.TableList.setModel(self.listModel) self.toolTableView.setModel(self.toolModel) self.loadData() self.toolTableView.resizeColumnsToContents() self.toolTableView.selectionModel().selectionChanged.connect(self.toolSelect) self.toolTableView.doubleClicked.connect(self.toolEdit) self.form.TableList.clicked.connect(self.tableSelected) self.form.toolAdd.clicked.connect(self.toolBitExisting) self.form.toolDelete.clicked.connect(self.toolDelete) self.form.toolCreate.clicked.connect(self.toolBitNew) self.form.addToolTable.clicked.connect(self.libraryNew) self.form.libraryOpen.clicked.connect(self.libraryPath) self.form.librarySave.clicked.connect(self.libraryOk) self.form.libraryExport.clicked.connect(self.librarySaveAs) self.toolSelect([], []) def librarySaveAs(self, path): """Save the tooltable to a format to use with an external system""" TooltableTypeJSON = translate("CAM_ToolBit", "Tooltable JSON (*.fctl)") TooltableTypeLinuxCNC = translate("CAM_ToolBit", "LinuxCNC tooltable (*.tbl)") TooltableTypeCamotics = translate("CAM_ToolBit", "CAMotics tooltable (*.json)") filename = PySide.QtGui.QFileDialog.getSaveFileName( self.form, translate("CAM_ToolBit", "Save toolbit library"), Path.Preferences.lastPathToolLibrary(), "{};;{};;{}".format(TooltableTypeJSON, TooltableTypeLinuxCNC, TooltableTypeCamotics), ) if filename and filename[0]: if filename[1] == TooltableTypeLinuxCNC: path = filename[0] if filename[0].endswith(".tbl") else "{}.tbl".format(filename[0]) self.libararySaveLinuxCNC(path) elif filename[1] == TooltableTypeCamotics: path = ( filename[0] if filename[0].endswith(".json") else "{}.json".format(filename[0]) ) self.libararySaveCamotics(path) else: path = ( filename[0] if filename[0].endswith(".fctl") else "{}.fctl".format(filename[0]) ) self.path = path self.librarySave() def libararySaveLinuxCNC(self, path): """Export the tool table to a file for use with linuxcnc""" LIN = "T{} P{} X{} Y{} Z{} A{} B{} C{} U{} V{} W{} D{} I{} J{} Q{}; {}" with open(path, "w") as fp: fp.write(";\n") for row in range(self.toolModel.rowCount()): toolNr = self.toolModel.data( self.toolModel.index(row, 0), PySide.QtCore.Qt.EditRole ) toolPath = self.toolModel.data(self.toolModel.index(row, 0), _PathRole) bit = PathToolBit.Factory.CreateFrom(toolPath) if bit: Path.Log.track(bit) pocket = bit.Pocket if hasattr(bit, "Pocket") else "0" xoffset = bit.Xoffset if hasattr(bit, "Xoffset") else "0" yoffset = bit.Yoffset if hasattr(bit, "Yoffset") else "0" zoffset = bit.Zoffset if hasattr(bit, "Zoffset") else "0" aoffset = bit.Aoffset if hasattr(bit, "Aoffset") else "0" boffset = bit.Boffset if hasattr(bit, "Boffset") else "0" coffset = bit.Coffset if hasattr(bit, "Coffset") else "0" uoffset = bit.Uoffset if hasattr(bit, "Uoffset") else "0" voffset = bit.Voffset if hasattr(bit, "Voffset") else "0" woffset = bit.Woffset if hasattr(bit, "Woffset") else "0" diameter = ( bit.Diameter.getUserPreferred()[0].split()[0] if hasattr(bit, "Diameter") else "0" ) frontangle = bit.FrontAngle if hasattr(bit, "FrontAngle") else "0" backangle = bit.BackAngle if hasattr(bit, "BackAngle") else "0" orientation = bit.Orientation if hasattr(bit, "Orientation") else "0" remark = bit.Label fp.write( LIN.format( toolNr, pocket, xoffset, yoffset, zoffset, aoffset, boffset, coffset, uoffset, voffset, woffset, diameter, frontangle, backangle, orientation, remark, ) + "\n" ) FreeCAD.ActiveDocument.removeObject(bit.Name) else: Path.Log.error("Could not find tool #{} ".format(toolNr)) def libararySaveCamotics(self, path): """Export the tool table to a file for use with camotics""" SHAPEMAP = { "ballend": "Ballnose", "endmill": "Cylindrical", "v-bit": "Conical", "chamfer": "Snubnose", } tooltemplate = { "units": "metric", "shape": "cylindrical", "length": 10, "diameter": 3.125, "description": "", } toollist = {} unitstring = "imperial" if FreeCAD.Units.getSchema() in [2, 3, 5, 7] else "metric" for row in range(self.toolModel.rowCount()): toolNr = self.toolModel.data(self.toolModel.index(row, 0), PySide.QtCore.Qt.EditRole) toolPath = self.toolModel.data(self.toolModel.index(row, 0), _PathRole) Path.Log.debug(toolPath) try: bit = PathToolBit.Factory.CreateFrom(toolPath) except FileNotFoundError as e: FreeCAD.Console.PrintError(e) continue except Exception as e: raise e if not bit: continue Path.Log.track(bit) toolitem = tooltemplate.copy() toolitem["diameter"] = ( float(bit.Diameter.getUserPreferred()[0].split()[0]) if hasattr(bit, "Diameter") else 2 ) toolitem["description"] = bit.Label toolitem["length"] = ( float(bit.Length.getUserPreferred()[0].split()[0]) if hasattr(bit, "Length") else 10 ) if hasattr(bit, "Camotics"): toolitem["shape"] = bit.Camotics else: toolitem["shape"] = SHAPEMAP.get(bit.ShapeName, "Cylindrical") toolitem["units"] = unitstring FreeCAD.ActiveDocument.removeObject(bit.Name) toollist[toolNr] = toolitem if len(toollist) > 0: with open(path, "w") as fp: fp.write(json.dumps(toollist, indent=2))