# -*- coding: utf-8 -*-
# Author: Ruslan Krenzler.
# Date: 06 February 2018
# Create a tee-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()+"/tee.csv"
DIMENSIONS_USED = ["G", "G1", "G2", "H", "H1", "H2", "PID", "PID1", "POD", "POD1", "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
def nestedObjects(group):
res = []
if group.OutList == []:
res.append(group)
else:
# Append children first.
for o in group.OutList:
res += nestedObjects(o)
res.append(group)
return res
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
class Error(Exception):
"""Base class for exceptions in this module."""
def __init__(self, message):
super(Error, self).__init__(message)
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):
super(UnplausibleDimensions, self).__init__(message)
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 the columns required to create a tee."""
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 Tee:
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.H = tu("4 in") # It is L/2 for symetrical Tee. Why extra dimension?
self.H1 = tu("5 in")
self.H2 = tu("6 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 += Tee.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.G > tu("0 mm") and self.G1 > tu("0 mm") and self.G2 > tu("0 mm")):
raise UnplausibleDimensions("Lengths G=%s, G1=%s, G=%s, must be positive"%(self.G, self.G1, self.G2))
if not ( self.H > self.G):
raise UnplausibleDimensions("H=%s must be larger than G=%s."%(self.H, self.G))
if not ( self.H1 > self.G1):
raise UnplausibleDimensions("H=%s must be larger than G=%s."%(self.H1, self.G1))
if not ( self.H2 > self.G2):
raise UnplausibleDimensions("H=%s must be larger than G=%s."%(self.H2, self.G2))
def create(self, convertToSolid):
self.checkDimensions()
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
vertical_inner_cylinder = self.document.addObject("Part::Cylinder","VerticalInnerCynlider")
vertical_inner_cylinder.Radius = self.PID1/2
vertical_inner_cylinder.Height =self.H1 * (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 tee, fuse inner parts to a tee, substract both parts
outer_fusion = self.document.addObject("Part::MultiFuse","OuterTeeFusion")
outer_fusion.Shapes = [vertical_outer_cylinder,horizontal_outer_cylinder]
inner_fusion = self.document.addObject("Part::MultiFuse","InnerTeeFusion")
inner_fusion.Shapes = [vertical_inner_cylinder,horizontal_inner_cylinder]
basic_tee = self.document.addObject("Part::Cut","Cut")
basic_tee.Base = outer_fusion
basic_tee.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 = App.Placement(App.Vector(0,0,self.G1), App.Rotation(App.Vector(0,1,0),0), App.Vector(0,0,0))
sockets_fusion = self.document.addObject("Part::MultiFuse","Sockets")
sockets_fusion.Shapes = [socket_left,socket_right,socket_top]
# remove sockets from the basic tee
tee = self.document.addObject("Part::Cut","Tee")
tee.Base = basic_tee
tee.Tool = sockets_fusion
if convertToSolid:
# Before making a solid, recompute documents. Otherwise there will be
# s = Part.Solid(Part.Shell(s))
#
Only dimensions used are: M, M1, G, G1, G2, H1, H1, H2, 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: tee = TeeFromTable(document, self.table) tee.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 tee fitting.\n"\ "Use menu File->New to create a new document first, "\ "then try to create the tee fitting again." msgBox = QtGui.QMessageBox(QtGui.QMessageBox.Warning, "Creating of the tee 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")) # Before working with macros, try to load the dimension table. 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(DIMENSIONS_USED) 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 return table # Test macros. def TestTee(): document = App.activeDocument() tee = Tee(document) tee.create(False) document.recompute() def TestTable(): document = App.activeDocument() table = CsvTable(DIMENSIONS_USED) table.load(CSV_TABLE_PATH) tee = TeeFromTable(document, table) for i in range(0, len(table.data)): print("Selecting row %d"%i) partName = table.getPartName(i) print("Creating part %s"%partName) part = tee.create(partName, False) document.recompute() document.recompute() # Create tee fitting. #TestTable() table = GuiCheckTable() # Open a CSV file, check its content, and return it as a CsvTable object. form = MainDialog(table)