# -*- coding: utf-8 -*- # Author: Ruslan Krenzler. # Date: 2 December 2017 # Create a pipe elbow. # Version 0.2 import math from PySide import QtCore, QtGui import FreeCAD import Spreadsheet import Sketcher import Part # Here are dimensions used to create a elbow. alpha = "60 deg" r1 = "1 in" r2 = "2 in" r3 = "3 in" len1 = "4 in" len2 = "5 in" tu = FreeCAD.Units.parseQuantity # Add sketches for swept part (circle) into X-Y Plane. document = App.activeDocument() 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 Elbow: # This factor 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. RELATIVE_EPSILON = 1.1 def __init__ (self, document): self.document = document # Set default values. self.alpha = "60 deg" self.r1 = "1 in" self.r2 = "2 in" self.r3 = "3 in" self.len1 = "4 in" self.len2 = "5 in" @staticmethod def createElbowCylinder(document, group, r_cyl, r_bent, alpha, len1, len2): """Create a cylinder with a radius r_cyl1 with a base on X-Y plane and bent it by angle alpha along radius r_bent. Add streight cylinders at the ends Add all new created objects to a group. This should simplify the cleaning up of the intermediate parts. :param r_cyl: radius of the cylinder in Base.Quantity :param r_bent: radius of the path in Base.Quantity :param alpha: in Base.Quantity :param len1: length of the streight part 1 :param len2: length of the streight part 2 """ base_sketch = document.addObject('Sketcher::SketchObject','BaseSketch') base_sketch.Placement = App.Placement(App.Vector(0.000000,0.000000,0.000000),App.Rotation(0.000000,0.000000,0.000000,1.000000)) # When adding a radius, do not forget to convert the units. base_sketch.addGeometry(Part.Circle(App.Vector(0.000000,0.000000,0),App.Vector(0,0,1),r_cyl),False) # Add sweeping part into X-Z plane. path_sketch = document.addObject('Sketcher::SketchObject','PathSketch') path_sketch.Placement = App.Placement(App.Vector(0.000000,0.000000,0.000000),App.Rotation(-0.707107,0.000000,0.000000,-0.707107)) # Note the pskecth is rotatet, therefore y and z coordinates are exchanged. # Add a line in botto direction. rel_epsilon = 0.0 line1 = Part.Line(App.Vector(0.000000,0.000000,0),App.Vector(-0.000000,-len1,0)) path_sketch.addGeometry(line1,False) # Add arc part. start = (tu("pi rad") - alpha).getValueAs("rad") stop = tu("pi rad").getValueAs("rad") arc = Part.ArcOfCircle(Part.Circle(App.Vector(r_bent,0,0),App.Vector(0,0,1),r_bent),start, stop) path_sketch.addGeometry(arc,False) # Find coordinates of the right point of the arc. x1 = (1-math.cos(alpha.getValueAs("rad")))*r_bent z1 = math.sin(alpha.getValueAs("rad"))*r_bent x2 = x1 + math.cos((tu("pi/2 rad")-alpha).getValueAs("rad"))*len2 z2 = z1 + math.sin((tu("pi/2 rad")-alpha).getValueAs("rad"))*len2 # Draw a streight line for the right pipe. line2 = Part.Line(App.Vector(x1,z1,0),App.Vector(x2,z2,0)) line2_geometry = path_sketch.addGeometry(line2,False) # Sweep the parts. sweep = document.addObject('Part::Sweep','Sweep') sweep.Sections=[base_sketch, ] sweep.Spine=(path_sketch,["Edge1", "Edge2", "Edge3"]) sweep.Solid=True sweep.Frenet=False # Add all the objects to the group. group.addObject(base_sketch) group.addObject(path_sketch,) group.addObject(sweep) return sweep @staticmethod def createElbowPart(document, group, r1, r3, alpha, len1, len2): # Make the outer part a little bit smaller, in order to prevent # problems with union of the pipe parts. # corrected_outer_radius = tu(r3)*(1-Elbow.BENT_RELATIVE_EPSILON) corrected_outer_radius = tu(r3) outer_sweep = Elbow.createElbowCylinder(document, group, corrected_outer_radius, tu(r3), tu(alpha), tu(len1), tu(len2)) inner_sweep = Elbow.createElbowCylinder(document, group, tu(r1), tu(r3), tu(alpha), tu(len1)*(1+Elbow.RELATIVE_EPSILON), tu(len2)*(1+Elbow.RELATIVE_EPSILON)) bent_cut = document.addObject("Part::Cut","BentCut") bent_cut.Base = outer_sweep bent_cut.Tool = inner_sweep group.addObject(bent_cut) return bent_cut @staticmethod def toSolid(document, part, name): """Convert object to a solid. Basically those are commands, which FreeCAD runs when you convert a part to a solid. Important!: recompute document. Without this step. The faces may not yet exists. (Is my interpretation correct?) document.recompute() """ 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 += Elbow.NestedObjects(o) res.append(group) return res def checkDimensions(self): if not ( (tu(self.alpha) > tu("0 deg")) and (tu(self.alpha) <= tu("180 deg")) ): raise UnplausibleDimensions("alpha %s must be within of range (0,180]"%self.alpha) if not ( tu(self.r1) > 0): raise UnplausibleDimensions("Radus r1=%s must be positive"%self.r1) if not ( tu(self.r2) >= tu(self.r1)): raise UnplausibleDimensions("Radus r2=%s must be greater than or equal to r1=%s"%(self.r2, self.r1)) if not ( tu(self.r3) >= tu(self.r2)): raise UnplausibleDimensions("Radus r3=%s must be greater than to r2=%s"%(self.r3, self.r2)) if not ( tu(self.len1) > 0): raise UnplausibleDimensions("Length len1=%s must be positive"%self.len1) if not ( tu(self.len2) > 0): raise UnplausibleDimensions("Length len2=%s must be positive"%self.len2) # raise UnplausibleDimensions("alpha %s out of range (0,180]"%self.alpha) # self.r1 = "1 in" # self.r2 = "2 in" # self.r3 = "3 in" # self.len1 = "4 in" # self.len2 = "5 in" def create(self, convertToSolid = True): self.checkDimensions() """Create elbow.""" # Create new group to put all the temporal data. group = self.document.addObject("App::DocumentObjectGroup", "elbow group") # Create the bent part. bent_part = self.createElbowPart(self.document, group, self.r1, self.r3, self.alpha, self.len1, self.len2) # Remove cyliders from both ends. inner_cylinder1 = document.addObject("Part::Cylinder","InnerCylinder1") inner_cylinder1.Radius = self.r2 inner_cylinder1.Height = tu(self.len1)*(1+Elbow.RELATIVE_EPSILON) inner_cylinder1.Placement.Base = App.Vector(0,0, -inner_cylinder1.Height) inner_cylinder2 = document.addObject("Part::Cylinder","InnerCylinder2") inner_cylinder2.Radius = self.r2 inner_cylinder2.Height = tu(self.len2)*(1+Elbow.RELATIVE_EPSILON) inner_cylinder2.Placement.Base = App.Vector(0,0, -inner_cylinder2.Height) x = (1-math.cos(tu(self.alpha).getValueAs("rad"))) *tu(self.r3) z = math.sin(tu(self.alpha).getValueAs("rad"))*tu(self.r3) inner_cylinder2.Placement = App.Placement(App.Vector(x,0,z),App.Rotation(App.Vector(0,1,0),tu(self.alpha))) cut1 = self.document.addObject("Part::Cut","PipeCut1") cut1.Base = bent_part cut1.Tool = inner_cylinder1 cut2 = self.document.addObject("Part::Cut","PipeCut2") cut2.Base = cut1 cut2.Tool = inner_cylinder2 group.addObject(cut2) if convertToSolid: # Remove objects of the pipes separately. # Create all faces. document.recompute() # Should I really recompute all the document? # Now convert all parts to solid, and remove intermediate data. elbow_solid = self.toSolid(document, cut2, "elbow (solid)") # Remove previous (intermediate parts). parts = Elbow.NestedObjects(group) # 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) document.removeObject(name) return elbow_solid return group NPS_NAME_COLUMN_INDEX = 0 NPS_SCHEDULE_COLUNN_INDEX = 1 NPS_OUTER_DIAMETER_COLUMN_INDEX = 2 NPS_INNER_DIAMETER_COLUMN_INDEX = 3 NPS_PVC_TABLE = [ ['1/8"', 40, 0.405,0.249,0.068,0.051,810], ['1/4"', 40, 0.540,0.344,0.088, 0.086, 780], ['3/8"', 40, 0.675, 0.473, 0.091, 0.115, 620], ['1/2"', 40, 0.840, 0.602, 0.109, 0.170, 600], ['3/4"', 40, 1.050, 0.804, 0.113, 0.226, 480], ['1"', 40, 1.315, 1.029, 0.133, 0.333, 450], ['1-1/4"', 40, 1.660, 1.360, 0.140, 0.450, 370], ['1-1/2"', 40, 1.900, 1.590, 0.145, 0.537, 330], ['2"', 40, 2.375, 2.047, 0.154, 0.720, 280], ['2-1/2"', 40, 2.875, 2.445, 0.203, 1.136, 300], ['3"', 40, 3.500, 3.042, 0.216, 1.488, 260], ['3-1/2"', 40, 4.000, 3.521, 0.226, 1.789, 240], ['4"', 40, 4.500, 3.998, 0.237, 2.118, 220], ['5"', 40, 5.563, 5.016, 0.258, 2.874, 190], ['6"', 40, 6.625, 6.031, 0.280, 3.733, 180], ['8"', 40, 8.625, 7.942, 0.322, 5.619, 160], ['10"', 40, 10.750, 9.976, 0.365, 7.966, 140], ['12"', 40, 12.750, 11.889, 0.406, 10.534, 130], ['1/8"',80, .405, .195, 0.095, 0.063, 1230], ['1/4"',80, .540, .282, 0.119, 0.105, 1130], ['3/8"',80, .675, .403, 0.126, 0.146, 920], ['1/2"',80, .840, .526, 0.147, 0.213, 850], ['3/4"',80, 1.050, .722, 0.154, 0.289, 690], ['1"',80, 1.315, .936, 0.179, 0.424, 630], ['1-1/4"',80, 1.660, 1.255, 0.191, 0.586, 520], ['1-1/2"',80, 1.900, 1.476, 0.200, 0.711, 470], ['2"',80, 2.375,1.913, 0.218, 0.984, 400], ['2-1/2"',80, 2.875, 2.290, 0.276, 1.500, 420], ['3"',80, 3.500, 2.864, 0.300, 2.010, 370], ['3-1/2"',80, 4.000, 3.326, 0.318, 2.452, 350], ['4"',80, 4.500, 3.786, 0.337, 2.938, 320], ['5"',80, 5.563, 4.768, 0.375, 4.078, 290], ['6"',80, 6.625, 5.709, 0.432, 5.610, 280], ['8"',80, 8.625, 7.565, 0.500, 8.522, 250], ['10"',80, 10.750, 9.493, 0.593, 12.635, 230], ['12"',80 ,12.750, 11.294, 0.687 ,17.384, 230]] # The dimensions MGH are from https://www.aetnaplastics.com/site_media/media/attachments/aetna_product_aetnaproduct/204/PVC%20Sch%2040%20Fittings%20Dimensions.pdf # Attention!: "1-13/16" in the table must be translated to "1+13.0/16" or to "1+13/16.0 ELBOW_90_NAME_COLUMN_INDEX = 0 ELBOW_90_PIPE_SIZE_NAME_COLUMN_INDEX = 1 ELBOW_90_SCHEDULE_COLUNN_INDEX = 2 ELBOW_90_G_COLUMN_INDEX = 3 ELBOW_90_H_COLUMN_INDEX = 4 ELBOW_90_M_COLUMN_INDEX = 5 ELBOW_90_HEADERS =["Part Number", "Size", "Schedule", "G", "H","M"] ELBOW_90_TABLE=[['406-003','3/8"' , 40, 3/8.0, 1+1/8.0, 7/8.0], ['406-005','1/2"', 40, 1/2.0, 1+1/4.0, 1+1/8.0], ['406-007', '3/4"', 40, 9.0/16, 1+1.0/2,1+11/32.0], ['406-010', '1"', 40, 11/16.0, 1+13/16.0, 1+5/8.0], ['406-012', '1-1/4"', 40, 31/32.0, 2-5/32.0, 2], ['406-015','1-1/2"', 40, 1+1/16.0, 2+11/32.0,2+1/4.0], ['406-020', '2"', 40, 1+5/160, 2-11/16.0, 2+3/4.0], ['406-025','2-1/2"', 40, 1+1/2.0, 3+1/4.0, 3+11/32.0], ['406-030', '3"', 40, 1+27/32.0, 3+23/32.0, 4], ['406-040', '4"', 40, 2+13/32.0, 4+13/32.0, 5+1/32.0], #['406-045F', '4-1/2"', 40, 8+5/8.0, 6+1/8.0, 5+7/16.0], # Impossible values. G>H. ['406-050', '5"', 40, 3+1/16.0, 6+1/8.0, 6+5/32.0], ['406-060','6"', 40, 3+17/32.0, 6+9/16.0, 7+9/32.0], ['406-080', '8"', 40, 4+13/32.0, 8+7/16.0, 9+3/8.0], ['406-100', '10"', 40, 5+13/16.0, 10+27/32.0, 11+19/32.0], ['406-100F', '10"', 40, 9+13/16.0, 15+1/16.0, 11+1/2.0], ['406-120', '12"', 40, 7+1/16.0, 13+9/16.0, 14+1/4.0], ['406-120F', '12"', 40, 11+3/16.0, 17+7/16.0, 13+9/16.0]] # For other entries, the pipe dimensions are missing. #['406-140', '14"', 7-1/2 14-1/2 15-11/16], #['406-140F', '14"', 13-3/16 20-3/16 14-7/8], #['406-160F', '16"', 15-1/4 23-1/4 17], #['406-180F', '18"', 17-5/16 26-5/16 19-1/8], #['406-200F', '20"', 19-13/16 29-13/16 22-1/16], #['406-240F', '24"', 23-7/16 35-7/16 25-3/8 216.00]] class Elbow90: def __init__ (self, document): self.document = document # Set default values. self.G = "10 mm" self.H = "20 mm" self.M = "20 mm" self.pipeOuterR = "8 mm" self.pipeInnerR = "6 mm" def create(self, convertToSolid = True): el = Elbow(document) el.alpha = "90 deg" el.r3 = str(tu(self.M)/2) el.r2 = self.pipeOuterR el.r1 = self.pipeInnerR el.len1 = str(tu(self.H)-tu(self.G)) el.len2 = el.len1 return el.create(convertToSolid) # Create a part following from the dimensions from NPS_PVC_TABLE. class Elbow90FromTable: def __init__ (self, document): self.document = document @staticmethod def findPipeDimensions(npsPipeSizeName, schedule): """Returns single row or None""" for row in NPS_PVC_TABLE: if row[NPS_NAME_COLUMN_INDEX]==npsPipeSizeName and row[NPS_SCHEDULE_COLUNN_INDEX]==schedule: return row return None @staticmethod def findPart(partName): for row in ELBOW_90_TABLE: if row[ELBOW_90_NAME_COLUMN_INDEX]==partName: return row return row def create(self, convertToSolid = True): el90 = Elbow90(self.document) row = Elbow90FromTable.findPart(self.partName) if row is None: print("Part not found") return el90.G = '%f in'%row[ELBOW_90_G_COLUMN_INDEX] el90.H = '%f in'%row[ELBOW_90_H_COLUMN_INDEX] el90.M = '%f in'%row[ELBOW_90_M_COLUMN_INDEX] # get pipe dimensions pipe_dims =Elbow90FromTable.findPipeDimensions(row[ELBOW_90_PIPE_SIZE_NAME_COLUMN_INDEX],row[ELBOW_90_SCHEDULE_COLUNN_INDEX]) if pipe_dims is None: print("No pipe dimensions found") return el90.pipeOuterR = str(pipe_dims[NPS_OUTER_DIAMETER_COLUMN_INDEX]/2.0)+'"' el90.pipeInnerR = str(pipe_dims[NPS_INNER_DIAMETER_COLUMN_INDEX]/2.0)+'"' return el90.create(convertToSolid) # Create multiple parts for testing. It is computation intensive. def Test(): ell = Elbow(document) x = tu("0 in") x_step = tu("20 in") # Create multiple parts for test purpose for alpha_deg in [30,45,60,90,110]: alpha = tu("%d deg"%alpha_deg) y = tu("0 in") for r3_in in range(1,10): r3 = tu("%d in"%r3_in) y = y + r3 # Move to the next position (part I). r2 = r3*2/3 r1 = r3/3 ell.alpha = str(alpha) ell.r3 = str(r3) ell.r2 = str(r2) ell.r1 = str(r1) label = "Angle: %s, Outer radius: %s"%(alpha.getUserPreferred()[0], r3.getUserPreferred()[0]) print("Creating... "+ label) part = ell.create(True) # Create a solid. #Add corresponding label to the part. part.Label = label Draft.move(part,FreeCAD.Vector(x,y,0),copy=False) y = y + r3 # Move to the next position (part II). x = x + x_step class CreateNpsPvcElbowGuiClass(QtGui.QDialog): """Return index of the row in the NPC_PVC library. Return -1 if nothing is selected. """ def __init__(self): super(CreateNpsPvcElbowGuiClass, self).__init__() self.initUi() def initUi(self): Dialog = self # Added self.result = -1 self.setupUi(self) # Fill table with dimensions. self.initTable() self.show() # The next lines are from QtDesigner .ui-file processed by pyside-uic def setupUi(self, Dialog): Dialog.setObjectName("Dialog") Dialog.resize(710, 298) self.buttonBox = QtGui.QDialogButtonBox(Dialog) self.buttonBox.setGeometry(QtCore.QRect(390, 260, 301, 32)) self.buttonBox.setOrientation(QtCore.Qt.Horizontal) self.buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Cancel|QtGui.QDialogButtonBox.Ok) self.buttonBox.setObjectName("buttonBox") self.tableViewParts = QtGui.QTableView(Dialog) self.tableViewParts.setGeometry(QtCore.QRect(10, 60, 691, 192)) self.tableViewParts.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) self.tableViewParts.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) self.tableViewParts.setObjectName("tableViewParts") self.checkBoxCreateSolid = QtGui.QCheckBox(Dialog) self.checkBoxCreateSolid.setGeometry(QtCore.QRect(10, 30, 171, 22)) self.checkBoxCreateSolid.setChecked(True) self.checkBoxCreateSolid.setObjectName("checkBoxCreateSolid") 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 90° elbow", None, QtGui.QApplication.UnicodeUTF8)) self.checkBoxCreateSolid.setText(QtGui.QApplication.translate("Dialog", "Create Solid", None, QtGui.QApplication.UnicodeUTF8)) def initTable(self): model = PartTableModel() self.tableViewParts.setModel(model) #self.tableViewParts.setModel(ELBOW_90_TABLE) #for row in ELBOW_90_TABLE: # self.tableViewParts.addItem(name) def getSelectedName(self): sel = form.tableViewParts.selectionModel() if sel.isSelected: if len(sel.selectedRows())> 0: rowIndex = sel.selectedRows()[0].row() #return model.getPartName(rowIndex) # This does not work. Why? return ELBOW_90_HEADERS[rowIndex][0] return None def accept(self): """User clicked OK""" # Get suitable row from the the table. partName = self.getSelectedName() createSolid = self.checkBoxCreateSolid.isChecked() if partName is not None: #self.storeInput() el = Elbow90FromTable(document) el.partName = partName el.create(createSolid) # Call parent class. document.recompute() super(CreateNpsPvcElbowGuiClass, self).accept() else: msgBox = QtGui.QMessageBox() msgBox.setText("Select part") msgBox.exec_() class PartTableModel(QtCore.QAbstractTableModel): def __init__(self, parent=None, *args): QtCore.QAbstractTableModel.__init__(self, parent, *args) def rowCount(self, parent): return len(ELBOW_90_TABLE) def columnCount(self, parent): return len(ELBOW_90_TABLE[0]) def data(self, index, role): if not index.isValid(): #return QtCore.QVariant() return None elif role != QtCore.Qt.DisplayRole: #return QtCore.QVariant() return None #return QtCore.QVariant(ELBOW_90_TABLE[index.row()][index.column()]) return ELBOW_90_TABLE[index.row()][index.column()] def getPartName(self, rowIndex): return ELBOW_90_HEADERS[rowIndex][0] def headerData(self, col, orientation, role): if orientation ==QtCore. Qt.Horizontal and role == QtCore.Qt.DisplayRole: return ELBOW_90_HEADERS[col] return None # Create all pipes from the 90°-elbow-table def Test2(): el = Elbow90FromTable(document) for row in ELBOW_90_TABLE: el.partName = row[ELBOW_90_NAME_COLUMN_INDEX] print("Creating part %s",el.partName) el.create(True) document.recompute() # Show Dialog #form = CreateNpsPvcElbowGuiClass() #form.exec_() # What is it for? #Test() # This needs a some time to calculate. #Test2()# This needs a some time to calculate. document.recompute()