# -*- coding: utf-8 -*-
# Author: Ruslan Krenzler.
# Date: 27 Januar 2018
# Create a bushing-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()+"/bushing.csv"
# It must contain unique values in the column "Name" and also, dimensions listened below.
DIMENSIONS_USED = ["PID", "PID1", "POD1", "L", "N"]
# 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 Bushing:
def __init__(self, document):
self.document = document
self.PID = tu("4 in")
self.PID1 = tu("1 in")
self.POD1 = tu("2 in")
self.N = tu("2 in")
self.L = tu("3 in")
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.POD1 > self.PID1):
raise UnplausibleDimensions("Outer diameter POD1 %s must be larger than inner diameter PID1 %s"%(self.POD1, self.PID1))
if not ( self.N > 0):
raise UnplausibleDimensions("Length N=%s must be positive"%self.X1)
if not ( self.PID > self.POD1):
raise UnplausibleDimensions("Inner diameter of the larger pipe PID %s must be larger than outer diameter of the smaller pipe POD1 %s."%(self.PID, self.POD1))
if not ( self.L > self.N):
raise UnplausibleDimensions("The liength L %s must be larger than the length N %s"%(self.L, self.N))
def createThing(self):
# Create Octagonal thing. I do not know its name.
# I do not know how to calculate X, there fore I just
# take a half of (L-N)
X1 = (self.L-self.N)/2
# I also do not know what is the size of the thing.
# I take 1.2 of the inner diameter of the larger pipe
X2 = self.PID*1.1
box1 = self.document.addObject("Part::Box","Box")
box1.Height = X1
box1.Length = X2
box1.Width = X2
# Move the box into the center
center = App.Vector(-X2/2, -X2/2,0)
box1.Placement.Base = center
# Add another box, but rotated by 45° around the z axis.
box2 = self.document.addObject("Part::Box","Box")
box2.Height = box1.Height
box2.Length = box1.Length
box2.Width = box1.Width
box2.Placement.Base = box1.Placement.Base
import Draft
Draft.rotate([box2],45,FreeCAD.Vector(0.0,0.0,0.0),axis=FreeCAD.Vector(0.0,0.0,1.0),copy=False)
# Cut both boxes
common = self.document.addObject("Part::MultiCommon","Common")
common.Shapes = [box1,box2,]
# Put the thing at the top of the bushing
common.Placement.Base = App.Vector(0,0,self.L-X1)
return common
def createOuterPart(self):
outer_cylinder = self.document.addObject("Part::Cylinder","OuterCynlider")
outer_cylinder.Radius = self.PID/2
outer_cylinder.Height = self.L
thing = self.createThing()
# Bind two parts.
fusion = self.document.addObject("Part::MultiFuse","Fusion")
fusion.Shapes = [outer_cylinder,thing,]
return fusion
def create(self, convertToSolid):
self.checkDimensions()
outer = self.createOuterPart()
# Remove inner part of the sockets.
inner_cylinder = self.document.addObject("Part::Cylinder","OuterCynlider")
inner_cylinder.Radius = self.PID1/2
inner_cylinder.Height = self.L
inner_socket = self.document.addObject("Part::Cylinder","OuterCynlider")
inner_socket.Radius = self.POD1/2
inner_socket.Height = self.L - self.N
inner_socket.Placement.Base = App.Vector(0,0,self.N)
# Make a cone for a larger socket. There are no dimensions for this con. There fore
# use simbolically a Radius such that the wall at the lower end is twice as ting
# as in the upper end of socket.
W2 = (self.PID-self.PID1)/2
socket_cone = self.document.addObject("Part::Cone","Cone")
socket_cone.Radius2 = self.PID1/2
socket_cone.Radius1 = self.PID1/2 + W2/2
socket_cone.Height = self.N/2 # I do not know what the hight of the cone should be.
# I just take a half.
inner = self.document.addObject("Part::MultiFuse","Fusion")
inner.Shapes = [inner_cylinder,inner_socket,socket_cone]
bushing = self.document.addObject("Part::Cut","Cut")
bushing.Base = outer
bushing.Tool = inner
if convertToSolid:
# Before making a solid, recompute documents. Otherwise there will be
# s = Part.Solid(Part.Shell(s))
#
To construct a part, only these dimensions are used: L, N, PID, PID1, and POD1. 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 partDimensionsSupported(self, partName): row = self.table.findPart(partName) """Return true if the dimension of the bushing are supported and False otherwise. Also return message as the second argument """ PID = row["PID"] POD1 = row["POD1"] if not (tu(PID) > tu(POD1)): return (False, ("This version does not support bushing whith PID %s" "smaller than POD1 %s.")%(PID, POD1)) else: return(True, "") 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: bushing = BushingFromTable(document, self.table) (supported, message) = self.partDimensionsSupported(partName) if supported: bushing.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(message) msgBox.exec_() else: msgBox = QtGui.QMessageBox() msgBox.setText("Select part") msgBox.exec_() else: text = "I have not found any active document were I can create a bushing fitting.\n"\ "Use menu File->New to create a new document first, "\ "then try to create the bushing fitting again." msgBox = QtGui.QMessageBox(QtGui.QMessageBox.Warning, "Creating of the bushing 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 bushing 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 bushing failed.", text) msgBox.exec_() exit(1) # Error return table # Test macros. def TestBushing(): document = App.activeDocument() bushing = Bushing(document) bushing.create(True) document.recompute() def TestTable(): document = App.activeDocument() table = CsvTable(DIMENSIONS_USED) table.load(CSV_TABLE_PATH) bushing = BushingFromTable(document, table) for i in range(0, len(table.data)): print("Selecting row %d"%i) partName = table.getPartName(i) print("Creating part %s"%partName) bushing.create(partName, True) document.recompute() break #TestBushing() #TestTable() table = GuiCheckTable() # Open a CSV file, check its content, and return it as a CsvTable object. form = MainDialog(table)