# -*- coding: utf-8 -*- # Author: Ruslan Krenzler. # Date: 27 Januar 2018 # Create a cross-fitting. # Version 0.3 import math import csv import os.path from PySide import QtCore, QtGui import FreeCAD import Spreadsheet import Sketcher import Part tu = FreeCAD.Units.parseQuantity def GetMacroPath(): param = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Macro") return param.GetString("MacroPath","") # This is the path to the dimensions table. CSV_TABLE_PATH = GetMacroPath()+"/cross.csv" # It must contain unique values in the column "Name" and also, dimensions listened below. DIMENSIONS_USED = ["POD", "PID", "POD1", "PID1", "G", "G1", "G2", "G3", "H", "H1", "H2", "H3", "M", "M1"] # The value RELATIVE_EPSILON is used to slightly change the size of a subtracted part # to prevent problems with boolean operations. # This value does not change the appearance of part and can be large. # If the original value is L then we often use the value L*(1+RELATIVE_EPSILON) instead. # The relative deviation is then (L*(1+RELATIVE_EPSILON)-L)/L = RELATIVE_EPSILON. # That is why the constant has "relative" in its name. # On my version of freecad 0.16 The macro works even with RELATIVE_EPSILON = 0.0. # Maybe there is no more problems with boolean operations. RELATIVE_EPSILON = 0.1 class Cross: def __init__(self, document): self.document = document # Fill data with test values self.G = tu("3 in") self.G1 = tu("3 in") self.G2 = tu("3 in") self.G3 = tu("3 in") self.H = tu("4 in") # It is L/2 for symetrical cross. Why extra dimension in documentation? self.H1 = tu("5 in") self.H2 = tu("6 in") self.H3 = tu("7 in") self.PID = tu("2 in") self.PID1 = tu("1 in") self.POD = tu("3 in") self.POD1 = tu("2 in") self.M = tu("5 in") self.M1 = tu("4 in") @staticmethod def toSolid(document, part, name): """Convert object to a solid. Basically those are commands, which FreeCAD runs when user converts a part to a solid. """ s = part.Shape.Faces s = Part.Solid(Part.Shell(s)) o = document.addObject("Part::Feature", name) o.Label=name o.Shape=s return o @staticmethod def NestedObjects(group): res = [] if group.OutList == []: res.append(group) else: # Append children first. for o in group.OutList: res += Cross.NestedObjects(o) res.append(group) return res def create(self, convertToSolid): L = self.H+self.H2 vertical_outer_cylinder = self.document.addObject("Part::Cylinder","VerticalOuterCynlider") vertical_outer_cylinder.Radius = self.M1/2 vertical_outer_cylinder.Height = self.H1+self.H3 vertical_outer_cylinder.Placement.Base = App.Vector(0,0,-self.H3) vertical_inner_cylinder = self.document.addObject("Part::Cylinder","VerticalInnerCynlider") vertical_inner_cylinder.Radius = self.PID1/2 vertical_inner_cylinder.Height = vertical_outer_cylinder.Height * (1+RELATIVE_EPSILON) vertical_inner_cylinder.Placement.Base = App.Vector(0,0,-self.H3 *(1+RELATIVE_EPSILON)) horizontal_outer_cylinder = self.document.addObject("Part::Cylinder","HorizontalOuterCynlider") horizontal_outer_cylinder.Radius = self.M/2 horizontal_outer_cylinder.Height = L # I do not understand the logic here. Why when I use GUI the vector is App.Vector(0,0,-L/2) # and with the macros it is App.Vector(-L/2,0,0). Differne systems? horizontal_outer_cylinder.Placement = App.Placement(App.Vector(-self.H,0,0), App.Rotation(App.Vector(0,1,0),90), App.Vector(0,0,0)) horizontal_inner_cylinder = self.document.addObject("Part::Cylinder","HorizontalInnerCynlider") horizontal_inner_cylinder.Radius = self.PID/2 horizontal_inner_cylinder.Height = L*(1+RELATIVE_EPSILON) horizontal_inner_cylinder.Placement = App.Placement(App.Vector(-self.H*(1+RELATIVE_EPSILON),0,0), App.Rotation(App.Vector(0,1,0),90), App.Vector(0,0,0)) # Fuse outer parts to a cross, fuse inner parts to a cross, substract both parts outer_fusion = self.document.addObject("Part::MultiFuse","OuterCrossFusion") outer_fusion.Shapes = [vertical_outer_cylinder,horizontal_outer_cylinder] inner_fusion = self.document.addObject("Part::MultiFuse","InnerCrossFusion") inner_fusion.Shapes = [vertical_inner_cylinder,horizontal_inner_cylinder] basic_cross = self.document.addObject("Part::Cut","Cut") basic_cross.Base = outer_fusion basic_cross.Tool = inner_fusion # Remove place for sockets. socket_left = self.document.addObject("Part::Cylinder","SocketLeft") socket_left.Radius = self.POD /2 socket_left.Height = (self.H-self.G)*(1+RELATIVE_EPSILON) socket_left.Placement = App.Placement(App.Vector(-socket_left.Height - self.G,0,0), App.Rotation(App.Vector(0,1,0),90), App.Vector(0,0,0)) # socket_left.Placement = App.Placement(App.Vector(-(self.H-self.G),0,0), App.Rotation(App.Vector(0,1,0),90), App.Vector(0,0,0)) socket_right = self.document.addObject("Part::Cylinder","SocketRight") socket_right.Radius = self.POD /2 socket_right.Height = (self.H2-self.G2)*(1+RELATIVE_EPSILON) socket_right.Placement = App.Placement(App.Vector(self.G2,0,0), App.Rotation(App.Vector(0,1,0),90), App.Vector(0,0,0)) socket_top = self.document.addObject("Part::Cylinder","SocketTop") socket_top.Radius = self.POD1 /2 socket_top.Height = (self.H1 - self.G1)*(1+RELATIVE_EPSILON) socket_top.Placement.Base = App.Vector(0,0,self.G1) socket_bottom = self.document.addObject("Part::Cylinder","SocketBottom") socket_bottom.Radius = self.POD1 /2 socket_bottom.Height = (self.H3 - self.G3)*(1+RELATIVE_EPSILON) socket_bottom.Placement.Base = App.Vector(0,0,-socket_bottom.Height-self.G3) sockets_fusion = self.document.addObject("Part::MultiFuse","Sockets") sockets_fusion.Shapes = [socket_left,socket_right,socket_top,socket_bottom] # Remove sockets from the basic cross cross = self.document.addObject("Part::Cut","Cross") cross.Base = basic_cross cross.Tool = sockets_fusion if convertToSolid: # Before making a solid, recompute documents. Otherwise there will be # s = Part.Solid(Part.Shell(s)) # : Shape is null # exception. self.document.recompute() # Now convert all parts to solid, and remove intermediate data. solid = self.toSolid(self.document, cross, "cross (solid)") # Remove previous (intermediate parts). parts = Cross.NestedObjects(cross) # Document.removeObjects can remove multple objects, when we use # parts directly. To prevent exceptions with deleted objects, # use the name list instead. names_to_remove = [] for part in parts: if part.Name not in names_to_remove: names_to_remove.append(part.Name) for name in names_to_remove: print("Deleting temporary objects %s."%name) self.document.removeObject(name) return solid return cross class CrossFromTable: """Create a part with dimensions from a CSV table.""" def __init__ (self, document, table): self.document = document self.table = table def create(self, partName, convertToSolid = True): cross = Cross(self.document) row = self.table.findPart(partName) if row is None: print("Part not found") return cross.G = tu(row["G"]) cross.G1 = tu(row["G1"]) cross.G2 = tu(row["G2"]) cross.G3 = tu(row["G3"]) cross.H = tu(row["H"]) cross.H1 = tu(row["H1"]) cross.H2 = tu(row["H2"]) cross.H3 = tu(row["H3"]) cross.PID = tu(row["PID"]) cross.PID1 = tu(row["PID1"]) cross.POD = tu(row["POD"]) cross.POD1 = tu(row["POD1"]) cross.M = tu(row["M"]) cross.M1 = tu(row["M1"]) part = cross.create(convertToSolid) part.Label = partName return part class CsvTable: """ Read coupling dimensions from a csv file. one part of the column must be unique and contains a unique key. It is the column "Name". """ def __init__(self, mandatoryDims=[]): """ @param mandatoryDims: list of column names which must be presented in the CSV files apart the "Name" column """ self.headers = [] self.data = [] self.hasValidData = False self.mandatoryDims=mandatoryDims def load(self, filename): """Load data from a CSV file.""" self.hasValidData = False with open(filename, "r") as csvfile: csv_reader = csv.reader(csvfile, delimiter=',', quotechar='"') self.headers = csv_reader.next() # Fill the talble self.data = [] names = [] ni = self.headers.index("Name") for row in csv_reader: # Check if the name is unique name = row[ni] if name in names: print('Error: Not unique name "%s" found in %s'%(name, filename)) exit(1) else: names.append(name) self.data.append(row) csvfile.close() # Should I close this file explicitely? self.hasValidData = self.hasNecessaryColumns() def hasNecessaryColumns(self): """ Check if the data contains all calumns required to create a coupling.""" return all(h in self.headers for h in (self.mandatoryDims + ["Name"])) def findPart(self, name): """Return first first raw with the particular part name as a dictionary.""" # First find out the index of the column "Name". ci = self.headers.index("Name") # Search for the first appereance of the name in this column. for row in self.data: if row[ci] == name: # Convert row to dicionary. return dict(zip(self.headers, row)) return None def getPartName(self, index): """Return part name of a row with the index *index*.""" ci = self.headers.index("Name") return self.data[index][ci] class PartTableModel(QtCore.QAbstractTableModel): def __init__(self, headers, data, parent=None, *args): self.headers = headers self.table_data = data QtCore.QAbstractTableModel.__init__(self, parent, *args) def rowCount(self, parent): return len(self.table_data) def columnCount(self, parent): return len(self.headers) def data(self, index, role): if not index.isValid(): return None elif role != QtCore.Qt.DisplayRole: return None return self.table_data[index.row()][index.column()] def getPartName(self, rowIndex): name_index = self.headers.index("Name") return self.table_data[rowIndex][name_index] def getPartRowIndex(self, partName): """ Return row index of the part with name partName. :param :partName name of the part :return: index of the first row whose part name is equal to partName return -1 if no row find. """ name_index = self.headers.index("Name") for row_i in range(name_index, len(self.table_data)): if self.table_data[row_i][name_index] == partName: return row_i return -1 def headerData(self, col, orientation, role): if orientation ==QtCore. Qt.Horizontal and role == QtCore.Qt.DisplayRole: return self.headers[col] return None class MainDialog(QtGui.QDialog): QSETTINGS_APPLICATION = "OSE piping freecad macros" QSETTINGS_NAME = "tee user input" def __init__(self, table): super(MainDialog, self).__init__() self.table = table self.initUi() def initUi(self): Dialog = self # Added self.result = -1 self.setupUi(self) # Fill table with dimensions. self.initTable() # Restore previous user input. Ignore exceptions to prevent this part # part of the code to prevent GUI from starting, once settings are broken. try: self.restoreInput() except Exception as e: print ("Could not restore old user input!") print(e) self.show() # The following lines are from QtDesigner .ui-file processed by pyside-uic # pyside-uic --indent=0 create-cross.ui -o tmp.py # # The file paths needs to be adjusted manually. For example # self.label.setPixmap(QtGui.QPixmap( GetMacroPath()+"/cross-dimensions.png")) # access datata in some special FreeCAD directory. def setupUi(self, Dialog): Dialog.setObjectName("Dialog") Dialog.resize(803, 666) self.verticalLayout = QtGui.QVBoxLayout(Dialog) self.verticalLayout.setObjectName("verticalLayout") self.checkBoxCreateSolid = QtGui.QCheckBox(Dialog) self.checkBoxCreateSolid.setChecked(True) self.checkBoxCreateSolid.setObjectName("checkBoxCreateSolid") self.verticalLayout.addWidget(self.checkBoxCreateSolid) self.tableViewParts = QtGui.QTableView(Dialog) self.tableViewParts.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) self.tableViewParts.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) self.tableViewParts.setObjectName("tableViewParts") self.verticalLayout.addWidget(self.tableViewParts) self.label_2 = QtGui.QLabel(Dialog) self.label_2.setTextFormat(QtCore.Qt.AutoText) self.label_2.setWordWrap(True) self.label_2.setObjectName("label_2") self.verticalLayout.addWidget(self.label_2) self.label = QtGui.QLabel(Dialog) self.label.setText("") self.label.setPixmap(QtGui.QPixmap(GetMacroPath()+"/cross-dimensions.png")) self.label.setAlignment(QtCore.Qt.AlignCenter) self.label.setObjectName("label") self.verticalLayout.addWidget(self.label) self.buttonBox = QtGui.QDialogButtonBox(Dialog) self.buttonBox.setOrientation(QtCore.Qt.Horizontal) self.buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Cancel|QtGui.QDialogButtonBox.Ok) self.buttonBox.setObjectName("buttonBox") self.verticalLayout.addWidget(self.buttonBox) self.retranslateUi(Dialog) QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL("accepted()"), Dialog.accept) QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL("rejected()"), Dialog.reject) QtCore.QMetaObject.connectSlotsByName(Dialog) def retranslateUi(self, Dialog): Dialog.setWindowTitle(QtGui.QApplication.translate("Dialog", "Create cross", None, QtGui.QApplication.UnicodeUTF8)) self.checkBoxCreateSolid.setText(QtGui.QApplication.translate("Dialog", "Create Solid", None, QtGui.QApplication.UnicodeUTF8)) self.label_2.setText(QtGui.QApplication.translate("Dialog", "

Only dimensions used are: M, M1, G, G1, G2, G3, H1, H2, H3, POD, POD1, PID, PID2. All other dimensions are used for inromation.

", None, QtGui.QApplication.UnicodeUTF8)) def initTable(self): # Read table data from CSV self.model = PartTableModel(self.table.headers, self.table.data) self.tableViewParts.setModel(self.model) def getSelectedPartName(self): sel = form.tableViewParts.selectionModel() if sel.isSelected: if len(sel.selectedRows())> 0: rowIndex = sel.selectedRows()[0].row() return self.model.getPartName(rowIndex) return None def selectPartByName(self, partName): """Select first row with a part with a name partName.""" if partName is not None: row_i = self.model.getPartRowIndex(partName) if row_i >= 0: self.tableViewParts.selectRow(row_i) def accept(self): """User clicked OK""" # Update active document. If there is none, show a warning message and do nothing. document = App.activeDocument() if document is not None: # Get suitable row from the the table. partName = self.getSelectedPartName() createSolid = self.checkBoxCreateSolid.isChecked() if partName is not None: cross = CrossFromTable(document, self.table) cross.create(partName, createSolid) document.recompute() # Save user input for the next dialog call. self.saveInput() # Call parent class. super(MainDialog, self).accept() else: msgBox = QtGui.QMessageBox() msgBox.setText("Select part") msgBox.exec_() else: text = "I have not found any active document were I can create a cross fitting.\n"\ "Use menu File->New to create a new document first, "\ "then try to create the cross fitting again." msgBox = QtGui.QMessageBox(QtGui.QMessageBox.Warning, "Creating of the cross fitting failed.", text) msgBox.exec_() def saveInput(self): """Store user input for the next run.""" settings = QtCore.QSettings(MainDialog.QSETTINGS_APPLICATION, MainDialog.QSETTINGS_NAME) check = self.checkBoxCreateSolid.checkState() settings.setValue("checkBoxCreateSolid", int(check)) settings.setValue("LastSelectedPartName", self.getSelectedPartName()) settings.sync() def restoreInput(self): settings = QtCore.QSettings(MainDialog.QSETTINGS_APPLICATION, MainDialog.QSETTINGS_NAME) checkState = QtCore.Qt.CheckState(int(settings.value("checkBoxCreateSolid"))) self.checkBoxCreateSolid.setCheckState(checkState) self.selectPartByName(settings.value("LastSelectedPartName")) def GuiCheckTable(): # Check if the CSV file exists. if os.path.isfile(CSV_TABLE_PATH) == False: text = "This macro requires %s but this file does not exist."%(CSV_TABLE_PATH) msgBox = QtGui.QMessageBox(QtGui.QMessageBox.Warning, "Creating of the tee failed.", text) msgBox.exec_() exit(1) # Error print("Trying to load CSV file with dimensions: %s"%CSV_TABLE_PATH) table = CsvTable() table.load(CSV_TABLE_PATH) if table.hasValidData == False: text = 'Invalid %s.\n'\ 'It must contain columns %s.'%(CSV_TABLE_PATH, ", ".join(DIMENSIONS_USED)) msgBox = QtGui.QMessageBox(QtGui.QMessageBox.Warning, "Creating of the tee failed.", text) msgBox.exec_() exit(1) # Error # Before working with macros, try to load the dimension table. # Test macros. def TestCross(): document = App.activeDocument() cross = Cross(document) cross.create(True) document.recompute() # Test macro. def TestTable(): document = App.activeDocument() table = CsvTable(DIMENSIONS_USED) table.load(CSV_TABLE_PATH) cross = CrossFromTable(document, table) for i in range(0, len(table.data)): print("Selecting row %d"%i) partName = table.getPartName(i) print("Creating part %s"%partName) cross.create(partName, False) document.recompute() # Create cross fitting. #TestCross() #TestTable() table = CsvTable() table.load(CSV_TABLE_PATH) form = MainDialog(table)