# -*- coding: utf-8 -*- # Author: Ruslan Krenzler. # Date: 20 Januar December 2018 # Create a coupling fitting. # Version 0.3 import math import csv import os.path from PySide import QtCore, QtGui import FreeCAD import Spreadsheet import Sketcher import Part # Note only columns "Name", "POD", "PID", "POD1", "PID1", "L", "M", "M1", and "N" # are used for calculations, all other are used for information. For example # "PipeSize", "Schedule", "PipeSize1" show pipe sizing in more readable form. tu = FreeCAD.Units.parseQuantity class CsvTable: """ Read coupling dimensions from a csv file""" def __init__(self): self.headers = [] self.data = [] self.hasValidData = False 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 ["Name", "POD", "PID", "POD1", "PID1", "L", "M", "M1", "N"]) 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 Error(Exception): """Base class for exceptions in this module.""" pass class UnplausibleDimensions(Error): """Exception raised when dimensions are unplausible. For example if outer diameter is larger than the iner one. Attributes: message -- explanation of the error """ def __init__(self, message): self.message = message class Coupling: def __init__(self, document): self.document = document # Set default values. self.M = tu("5 in") # Outer diameter of socket 1. self.POD = tu("4 in") # Pipe outer diameter at the socket 1. self.PID = tu("3 in") # Pipe inner diameter at the socket 1. self.X1 = tu("4 in") # Length of the socket1. self.M1 = tu("3 in") # Outer diameter of socket 2. self.POD1 = tu("2 in") # Pipe outer diameter at the socket 2. self.PID1 = tu("1 in") # Pipe inner diameter at the socket 2. self.X2 = tu("4 in") # Length of the socket2. self.N = tu("1 in") # Lenght of the intemidate section of the coupling. @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 += Coupling.NestedObjects(o) res.append(group) return res def checkDimensions(self): if not ( self.PID > tu("0 mm") and self.PID1 > tu("0 mm") ): raise UnplausibleDimensions("Inner pipe dimensions must be positive. They are %s and %s instead"%(self.PID, self.PID1)) if not ( self.M > self.POD and self.POD > self.PID ): raise UnplausibleDimensions("It must hold outer diameter %s > Outer pipe diameter %s > Inner pipe diameter %s"%(self.M, self.POD, self.PID)) if not ( self.M1 > self.POD1 and self.POD1 > self.PID1 ): raise UnplausibleDimensions("It must hold outer diameter %s > Outer pipe diameter %s > Inner pipe diameter %s"%(self.M1, self.POD1, self.PID1)) if not ( self.X1 > 0): raise UnplausibleDimensions("Length X1=%s must be positive"%self.X1) if not ( self.X2 > 0): raise UnplausibleDimensions("Length X2=%s must be positive"%self.X2) if not ( self.N > 0): raise UnplausibleDimensions("Intermediate length N=%s must be positive"%self.N) def calculateShiftA2(self): """Determine an additional length a2 of the socket 1, such that the wall size of the intermediate section on it thin part is not smaller than the walls of the sockets. The size a2 does not come from some document or standard. It is only chosen to avoid thin walls in the intermediate section of thecoupling. Probably a2 must be even larger. """ a2 = max(self.M-self.POD, self.M1-self.POD1) / 2 x = (self.POD-self.POD1) # The math.sqrt will return Float. That is why # we need to convert x in float too. factor = x.Value/math.sqrt(4*self.N**2+x**2) a1 = factor*a2 return a1 def createOuterPart(self): if self.M == self.M1: return self.createOuterPartEqual() else: return self.createOuterPartReduced() def createOuterPartEqual(self): """ Create a outer part which is cylinder only. This is when M and M1 are the same""" # Create socket 1. outer = self.document.addObject("Part::Cylinder","Cylinder") outer.Radius = self.M/2 outer.Height = self.X1+self.N+self.X2 return outer def createOuterPartReduced(self): """ Create a outer part which is cylinder+cone+cylinder.""" # Create socket 1. cylinder1 = self.document.addObject("Part::Cylinder","Cylinder1") cylinder1.Radius = self.M/2 a1 = self.calculateShiftA2() cylinder1.Height = self.X1+a1 # Create a cone and put it on the cylinder 1 cone = self.document.addObject("Part::Cone","Cone") cone.Radius1 = self.M/2 cone.Radius2 = self.M1/2 cone.Height = self.N cone.Placement.Base = App.Vector(0,0,cylinder1.Height) # Create a socket 2 and put it on the cone cylinder2 = self.document.addObject("Part::Cylinder","Cylinder2") cylinder2.Radius = self.M1/2 cylinder2.Height = self.X2-a1 cylinder2.Placement.Base = App.Vector(0,0,cylinder1.Height+cone.Height) # Combine all outer parts. outer = document.addObject("Part::MultiFuse","OuterParts") outer.Shapes = [cylinder1, cone, cylinder2] return outer def createInnerPart(self): # Create parts which must be removed from the coupling. if self.PID == self.PID1: return self.createInnerPartEqual() else: return self.createInnerPartReduced() def createInnerPartEqual(self): """ Create inner part from cylinders. This is when PID and PID1 are the same""" cylinder1i = self.document.addObject("Part::Cylinder","Cylinder1i") cylinder1i.Radius = self.POD/2 cylinder1i.Height = self.X1 cylinder3i = self.document.addObject("Part::Cylinder","Cylinder3i") cylinder3i.Radius = self.PID1/2 cylinder3i.Height = self.N cylinder3i.Placement.Base = App.Vector(0,0,cylinder1i.Height) cylinder2i = self.document.addObject("Part::Cylinder","Cylinder2i") cylinder2i.Radius = self.POD1/2 cylinder2i.Height = self.X2 cylinder2i.Placement.Base = App.Vector(0,0,cylinder1i.Height+cylinder3i.Height) inner = document.addObject("Part::MultiFuse","InnerParts") inner.Shapes = [cylinder1i, cylinder3i, cylinder2i] return inner def createInnerPartReduced(self): """ Create a outer part which is cylinder+cone+cylinder.""" cylinder1i = self.document.addObject("Part::Cylinder","Cylinder1i") cylinder1i.Radius = self.POD/2 cylinder1i.Height = self.X1 conei = self.document.addObject("Part::Cone","Cone") conei.Radius1 = self.PID/2 conei.Radius2 = self.PID1/2 conei.Height = self.N conei.Placement.Base = App.Vector(0,0,cylinder1i.Height) cylinder2i = self.document.addObject("Part::Cylinder","Cylinder2i") cylinder2i.Radius = self.POD1/2 cylinder2i.Height = self.X2 cylinder2i.Placement.Base = App.Vector(0,0,cylinder1i.Height+conei.Height) inner = document.addObject("Part::MultiFuse","InnerParts") inner.Shapes = [cylinder1i, conei, cylinder2i] return inner def create(self, convertToSolid): self.checkDimensions() outer = self.createOuterPart() inner = self.createInnerPart() coupling = self.document.addObject("Part::Cut","coupling") coupling.Base = outer coupling.Tool = inner if convertToSolid: # Before making a solid, recompute documents. self.document.recompute() # Now convert all parts to solid, and remove intermediate data. solid = self.toSolid(self.document, coupling, "coupling (solid)") # Remove previous (intermediate parts). parts = Coupling.NestedObjects(coupling) # 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 coupling class CouplingFromTable: """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): coupling = Coupling(self.document) row = self.table.findPart(partName) if row is None: print("Part not found") return coupling.M = tu(row["M"]) # Outer diameter of socket 1. coupling.POD = tu(row["POD"]) # Pipe outer diameter at the socket 1. coupling.PID = tu(row["PID"]) # Pipe inner diameter at the socket 1. coupling.X1 = (tu(row["L"])-tu(row["N"]))/2# Length of the socket1. coupling.M1 = tu(row["M1"]) # Outer diameter of socket 2. coupling.POD1 = tu(row["POD1"]) # Pipe outer diameter at the socket 2. coupling.PID1 = tu(row["PID1"]) # Pipe inner diameter at the socket 2. coupling.X2 = coupling.X1 # Length of the socket2. coupling.N = tu(row["N"]) # Lenght of the intemidate section of the coupling. part = coupling.create(convertToSolid) part.Label = partName return part 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 GuiCoupling(QtGui.QDialog): QSETTINGS_APPLICATION = "OSE piping freecad macros" QSETTINGS_NAME = "coupling user input" def __init__(self, table): super(GuiCoupling, 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-coupling.ui -o tmp.py # # The file paths needs to be adjusted manually. For example # self.label.setPixmap(QtGui.QPixmap(App.ConfigGet("UserAppData")+"coupling-dimensions.png")) # access datata in some special FreeCAD directory. def setupUi(self, Dialog): Dialog.setObjectName("Dialog") Dialog.resize(803, 592) 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 = QtGui.QLabel(Dialog) self.label.setText("") self.label.setPixmap(App.ConfigGet("UserAppData")+"coupling-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 coupling", None, QtGui.QApplication.UnicodeUTF8)) self.checkBoxCreateSolid.setText(QtGui.QApplication.translate("Dialog", "Create Solid", 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: coupling = CouplingFromTable(document, self.table) coupling.create(partName, createSolid) document.recompute() # Save user input for the next dialog call. self.saveInput() # Call parent class. super(GuiCoupling, 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 coupling.\n"\ "Use menu File->New to create a new document first, "\ "then try to create the coupling again." msgBox = QtGui.QMessageBox(QtGui.QMessageBox.Warning, "Creating of the coupling failed.", text) msgBox.exec_() def saveInput(self): """Store user input for the next run.""" settings = QtCore.QSettings(GuiCoupling.QSETTINGS_APPLICATION, GuiCoupling.QSETTINGS_NAME) check = self.checkBoxCreateSolid.checkState() settings.setValue("checkBoxCreateSolid", int(check)) settings.setValue("LastSelectedPartName", self.getSelectedPartName()) settings.sync() def restoreInput(self): settings = QtCore.QSettings(GuiCoupling.QSETTINGS_APPLICATION, GuiCoupling.QSETTINGS_NAME) checkState = QtCore.Qt.CheckState(int(settings.value("checkBoxCreateSolid"))) self.checkBoxCreateSolid.setCheckState(checkState) self.selectPartByName(settings.value("LastSelectedPartName")) document = App.activeDocument() # Before working with macros, try to load the dimension table. CSV_TABLE_PATH = App.ConfigGet("UserAppData")+"coupling.csv" # 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 coupling 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 "Name", "POD", "PID", "POD1", "PID1", "L", "M", "M1", and "N".'%(CSV_TABLE_PATH) msgBox = QtGui.QMessageBox(QtGui.QMessageBox.Warning, "Creating of the coupling failed.", text) msgBox.exec_() exit(1) # Error # Test macro. def TestTable(): coupling = CouplingFromTable(document, table) for i in range(0, len(table.data)): print("Selecting row %d"%i) partName = table.getPartName(i) print("Creating part %s"%partName) coupling.create(partName, True) document.recompute() #coupling = CouplingFromTable(document, table) #coupling.create("429-212", False) #TestTable() #document.recompute() form = GuiCoupling(table)