Created shaft wizard (moved here from PartDesign branch)

This commit is contained in:
jrheinlaender 2012-09-22 11:57:44 +04:30 committed by wmayer
parent 0b5212e46b
commit 5c1948d8b6
10 changed files with 1324 additions and 12 deletions

View File

@ -29,3 +29,28 @@ INSTALL(
Mod/PartDesign/Scripts
)
SET(WizardShaft_SRCS
WizardShaft/__init__.py
WizardShaft/WizardShaft.py
WizardShaft/WizardShaftTable.py
WizardShaft/Shaft.py
WizardShaft/ShaftFeature.py
WizardShaft/ShaftDiagram.py
WizardShaft/SegmentFunction.py
)
SOURCE_GROUP("wizardshaft" FILES ${WizardShaft_SRCS})
SET(all_files ${WizardShaft_SRCS})
ADD_CUSTOM_TARGET(WizardShaft ALL
SOURCES ${all_files}
)
fc_copy_sources(Mod/PartDesign "${CMAKE_BINARY_DIR}/Mod/PartDesign" ${all_files})
INSTALL(
FILES
${WizardShaft_SRCS}
DESTINATION
Mod/PartDesign/WizardShaft
)

View File

@ -29,10 +29,9 @@
#* Juergen Riegel 2002 *
#***************************************************************************/
class PartDesignWorkbench ( Workbench ):
"PartDesign workbench object"
from WizardShaft import WizardShaft
Icon = """
/* XPM */
static char * partdesign_xpm[] = {

View File

@ -0,0 +1,76 @@
# PartDesign gui init module
# (c) 2003 Juergen Riegel
#
# Gathering all the information to start FreeCAD
# This is the second one of three init scripts, the third one
# runs when the gui is up
#***************************************************************************
#* (c) Juergen Riegel (juergen.riegel@web.de) 2002 *
#* *
#* This file is part of the FreeCAD CAx development system. *
#* *
#* This program is free software; you can redistribute it and/or modify *
#* it under the terms of the GNU General Public License (GPL) *
#* as published by the Free Software Foundation; either version 2 of *
#* the License, or (at your option) any later version. *
#* for detail see the LICENCE text file. *
#* *
#* FreeCAD is distributed in the hope that it will be useful, *
#* but WITHOUT ANY WARRANTY; without even the implied warranty of *
#* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
#* GNU Library General Public License for more details. *
#* *
#* You should have received a copy of the GNU Library General Public *
#* License along with FreeCAD; if not, write to the Free Software *
#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
#* USA *
#* *
#* Juergen Riegel 2002 *
#***************************************************************************/
class PartDesignWorkbench ( Workbench ):
"PartDesign workbench object"
from WizardShaft import WizardShaft
Icon = """
/* XPM */
static char * partdesign_xpm[] = {
"16 16 9 1",
" c None",
". c #040006",
"+ c #070F38",
"@ c #002196",
"# c #0030F3",
"$ c #5A4D20",
"% c #858EB2",
"& c #DEB715",
"* c #BFB99D",
" & ........ ",
"&&&$..@@@@@@+...",
"&&&&$@#####@..@.",
"&&&&&$......@#@.",
"&&&&&&@@@+.###@.",
"$&&&&&&@#@.###@.",
".$&&&&&%#@.###@.",
".@*&&&*%#@.###@.",
".@#*&**%#@.###@.",
".@#@%%%.@@.###@.",
".@@@@@@@#@.###@.",
".@#######@.###@.",
".@#######@.##+. ",
".+@@@####@.@.. ",
" ......+++.. ",
" ... "};
"""
MenuText = "Part Design"
ToolTip = "Part Design workbench"
def Initialize(self):
# load the module
import PartDesignGui
import PartDesign
def GetClassName(self):
return "PartDesignGui::Workbench"
Gui.addWorkbench(PartDesignWorkbench())

View File

@ -0,0 +1,152 @@
#/******************************************************************************
# * Copyright (c)2012 Jan Rheinlaender <jrheinlaender@users.sourceforge.net> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
# * This library is free software; you can redistribute it and/or *
# * modify it under the terms of the GNU Library General Public *
# * License as published by the Free Software Foundation; either *
# * version 2 of the License, or (at your option) any later version. *
# * *
# * This library is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this library; see the file COPYING.LIB. If not, *
# * write to the Free Software Foundation, Inc., 59 Temple Place, *
# * Suite 330, Boston, MA 02111-1307, USA *
# * *
# ******************************************************************************/
class SegmentFunctionSegment:
"One segment of a segment function"
start = 0
variable = "x"
coefficient = 0
exponent = 0
def __init__(self, st, var, coeff, exp):
self.start = st
self.variable = var
self.coefficient = coeff
self.exponent = exp
def hasStart(self, xval):
"Return true if the start of this segment is xval"
#FIXME: 1E-9 is arbitrary here. But since units are in meters, 1E-9 is a nanometer...
return abs(self.start - xval) < 1E-9
def value(self, xval):
if xval < self.start:
return 0
else:
return self.coefficient * pow(xval - self.start, self.exponent)
def clone(self):
return SegmentFunctionSegment(self.start, self.variable, self.coefficient, self.exponent)
def negate(self):
self.coefficient *= -1
def integrate(self):
self.exponent = self.exponent + 1
self.coefficient = self.coefficient * 1 / self.exponent
def asString(self):
return "%f * {%s - %f}^%i" % (self.coefficient, self.variable, self.start, self.exponent)
class SegmentFunction:
"Function that is defined segment-wise"
variable = "x"
segments = []
name = "f(x)"
def __init__(self, name = "f(x)"):
self.variable = "x"
self.segments = []
self.name = name
def negate(self):
for s in self.segments:
s.negate()
return self
def index(self, xval):
"Find insert position for start value xval"
lastStart = 0.0
for i in range(len(self.segments)):
newStart = self.segments[i].start
if (xval >= lastStart) and (xval < newStart):
return i
lastStart = newStart
return len(self.segments)
def buildFromDict(self, var, dict):
self.variable = var
for key in sorted(dict.iterkeys()):
#if abs(dict[key]) > 1E-9:
self.segments.append(SegmentFunctionSegment(key, var, dict[key], 0))
def addSegments(self, dict):
for key in sorted(dict.iterkeys()):
if abs(dict[key]) > 1E-9:
self.segments.insert(self.index(key), SegmentFunctionSegment(key, self.variable, dict[key], 0))
def setMaxX(self, mx):
self.maxX = mx
def value(self, xval):
"Return the value of the function at the specified x value"
result = 0
for s in self.segments:
result = result + s.value(xval)
return result
def lowervalue(self, xval):
"Return the value of the previous segment at the specified x value"
result = 0
for s in self.segments:
result = result + s.value(xval - 1E-8)
return result
def clone(self):
result = SegmentFunction()
result.variable = self.variable
for s in self.segments:
result.segments.append(s.clone())
return result
def integrate(self):
"Integrate all segments with respect to the variable"
for s in self.segments:
s.integrate()
def integrated(self):
"Return a copy of self integrated with respect to the variable"
result = self.clone()
result.integrate()
return result
def evaluate(self, maxX, pointsX):
# Note: This usually creates a few more points than specified in pointsX
offset = (maxX - self.segments[0].start) / (pointsX - 1)
xvals = set([self.segments[0].start + s * offset for s in range(pointsX)])
starts = set([self.segments[i].start for i in range(len(self.segments))])
xvals = xvals.union(starts) # Make sure we have a point on each segment start
result = []
for xval in sorted(xvals):
if xval in starts:
result.append( (xval, self.lowervalue(xval)) ) # create double point at segment border
result.append( (xval, self.value(xval)) )
return result
def output(self):
print self.name, " = ",
for i in range(len(self.segments)):
print self.segments[i].asString(),
if i < len(self.segments) - 1:
print " + ",
print ""

View File

@ -0,0 +1,258 @@
#/******************************************************************************
# * Copyright (c)2012 Jan Rheinlaender <jrheinlaender@users.sourceforge.net> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
# * This library is free software; you can redistribute it and/or *
# * modify it under the terms of the GNU Library General Public *
# * License as published by the Free Software Foundation; either *
# * version 2 of the License, or (at your option) any later version. *
# * *
# * This library is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this library; see the file COPYING.LIB. If not, *
# * write to the Free Software Foundation, Inc., 59 Temple Place, *
# * Suite 330, Boston, MA 02111-1307, USA *
# * *
# ******************************************************************************/
import os, tempfile
import FreeCADGui
import WebGui
from SegmentFunction import SegmentFunction
from ShaftFeature import ShaftFeature
from ShaftDiagram import Diagram
htmlHeader = """
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<HTML>
<HEAD>
<META HTTP-EQUIV="CONTENT-TYPE" CONTENT="text/html; charset=utf-8">
<TITLE></TITLE>
<META NAME="GENERATOR" CONTENT="FreeCAD shaft design wizard">
</HEAD>
<BODY LANG="en-US" DIR="A4">
<P>
"""
htmlFooter = """
</P>
</BODY>
</HTML>
"""
class ShaftSegment:
length = 0.0
diameter = 0.0
loadType = "None"
loadSize = 0.0
loadLocation = 0.0
def __init__(self, l, d):
self.length = l
self.diameter = d
class Shaft:
"The axis of the shaft is always assumed to correspond to the X-axis"
# List of shaft segments (each segment has a different diameter)
segments = []
# The sketch
sketch = 0
#featureWindow = None
# The diagrams
diagrams = {} # map of function name against Diagram object
# Directory for diagram files
tmpdir = ""
# HTML browser
haveBrowser = False
#htmlWindow = None
# Calculation of shaft
Qy = 0 # force in direction of y axis
Qz = 0 # force in direction of z axis
Mbz = 0 # bending moment around z axis
Mby = 0 # bending moment around y axis
Mtz = 0 # torsion moment around z axis
def __init__(self, doc):
# Create a temporary directory
self.tmpdir = tempfile.mkdtemp()
self.sketch = ShaftFeature(doc)
def getLengthTo(self, index):
"Get the total length of all segments up to the given one"
result = 0.0
for i in range(index):
result += self.segments[i].length
return result
def addSegment(self, l, d):
#print "Adding segment: ", l, " : ", d
self.segments.append(ShaftSegment(l,d))
self.sketch.addSegment(l, d)
# We don't call equilibrium() here because the new segment has no loads defined yet
def updateSegment(self, index, length = None, diameter = None):
oldLength = self.segments[index].length
#print "Old length of ", index, ": ", oldLength, ", new Length: ", length, " diameter: ", diameter
if length is not None:
self.segments[index].length = length
if diameter is not None:
self.segments[index].diameter = diameter
self.sketch.updateSegment(index, oldLength, self.segments[index].length, self.segments[index].diameter)
self.equilibrium()
self.updateDiagrams()
def updateLoad(self, index, loadType = None, loadSize = None, loadLocation = None):
if (loadType is not None):
self.segments[index].loadType = loadType
if (loadSize is not None):
self.segments[index].loadSize = loadSize
if (loadLocation is not None):
if (loadLocation >= 0) and (loadLocation <= self.segments[index].length):
self.segments[index].loadLocation = loadLocation
else:
# TODO: Show warning
print "Load location must be inside segment"
#self.feature.updateForces() graphical representation of the forces
self.equilibrium()
self.updateDiagrams()
def updateDiagrams(self):
if (self.Qy == 0) or (self.Mbz == 0):
return
if self.Qy.name in self.diagrams:
# Update diagram
self.diagrams[self.Qy.name].update(self.Qy, self.getLengthTo(len(self.segments)) / 1000.0)
else:
# Create diagram
self.diagrams[self.Qy.name] = Diagram(self.tmpdir)
self.diagrams[self.Qy.name].create("Shear force", self.Qy, self.getLengthTo(len(self.segments)) / 1000.0, "x", "mm", 1000.0, "Q_y", "N", 1.0, 10)
if self.Mbz.name in self.diagrams:
# Update diagram
self.diagrams[self.Mbz.name].update(self.Mbz, self.getLengthTo(len(self.segments)) / 1000.0)
else:
# Create diagram
self.diagrams[self.Mbz.name] = Diagram(self.tmpdir)
self.diagrams[self.Mbz.name].create("Bending moment", self.Mbz, self.getLengthTo(len(self.segments)) / 1000.0, "x", "mm", 1000.0, "M_{b,z}", "Nm", 1.0, 10)
if self.haveBrowser is False:
# Create HTML file with the diagrams
htmlFile = os.path.join(self.tmpdir, "diagrams.html")
file = open(htmlFile, "w")
file.write(htmlHeader)
for fname in self.diagrams.iterkeys():
plotFile = os.path.join(self.tmpdir, (fname + ".png"))
file.write("<IMG SRC=\"%s\" NAME=\"%s\" ALIGN=MIDDLE WIDTH=450 HEIGHT=320 BORDER=0>\n" % (plotFile, fname))
file.write(htmlFooter)
file.close()
WebGui.openBrowser(htmlFile)
self.haveBrowser = True
# Once the diagram is created, it will take care of refreshing the HTML view
def equilibrium(self):
# Build equilibrium equations
forces = {0.0:0.0} # dictionary of (location : outer force)
moments = {0.0:0.0} # dictionary of (location : outer moment)
variableNames = [""] # names of all variables
locations = {} # dictionary of (variableName : location)
coefficientsFy = [0] # force equilibrium equation
coefficientsMbz = [0] # moment equilibrium equation
for i in range(len(self.segments)):
lType = self.segments[i].loadType
load = -1 # -1 means unknown (just for debug printing)
location = -1
if lType == "Fixed":
# Fixed segment
if i == 0:
location = 0
variableNames.append("Fy%u" % i)
coefficientsFy.append(1)
coefficientsMbz.append(0)
variableNames.append("Mz%u" % i)
coefficientsFy.append(0)
coefficientsMbz.append(1) # Force does not contribute because location is zero
elif i == len(self.segments) - 1:
location = self.getLengthTo(len(self.segments)) / 1000
variableNames.append("Fy%u" % i)
coefficientsFy.append(1)
coefficientsMbz.append(location)
variableNames.append("Mz%u" % i)
coefficientsFy.append(0)
coefficientsMbz.append(1)
else:
# TODO: Better error message
print "Fixed constraint must be at beginning or end of shaft"
return
locations["Fy%u" % i] = location
locations["Mz%u" % i] = location
elif lType == "Static":
# Static load (currently force only)
load = self.segments[i].loadSize
location = (self.getLengthTo(i) + self.segments[i].loadLocation) / 1000 # convert to meters
coefficientsFy[0] = coefficientsFy[0] - load
forces[location] = load
coefficientsMbz[0] = coefficientsMbz[0] - load * location
moments[location] = 0
#elif lType == "None":
# # No loads on segment
print "Segment: %u, type: %s, load: %f, location: %f" % (i, lType, load, location)
self.printEquilibrium(variableNames, coefficientsFy)
self.printEquilibrium(variableNames, coefficientsMbz)
# Build matrix and vector for linear algebra solving algorithm
import numpy as np
if (len(coefficientsFy) < 3) or (len(coefficientsMbz) < 3):
return
A = np.array([coefficientsFy[1:], coefficientsMbz[1:]])
b = np.array([coefficientsFy[0], coefficientsMbz[0]])
solution = np.linalg.solve(A, b)
# Complete dictionary of forces and moments
if variableNames[1][0] == "F":
forces[locations[variableNames[1]]] = solution[0]
else:
moments[locations[variableNames[1]]] = solution[0]
if variableNames[2][0] == "F":
forces[locations[variableNames[2]]] = solution[1]
else:
moments[locations[variableNames[2]]] = solution[1]
print forces
print moments
self.Qy = SegmentFunction("Qy")
self.Qy.buildFromDict("x", forces)
self.Qy.output()
self.Mbz = self.Qy.integrated().negate()
self.Mbz.addSegments(moments) # takes care of boundary conditions
self.Mbz.name = "Mbz"
self.Mbz.output()
def printEquilibrium(self, var, coeff):
# Auxiliary method for debugging purposes
for i in range(len(var)):
if i == 0:
print "%f = " % coeff[i],
else:
print "%f * %s" % (coeff[i], var[i]),
if (i < len(var) - 1) and (i != 0):
print " + ",
print ""
def __del__(self):
"Remove the temporary directory"
for fname in self.diagrams.iterkeys():
os.remove(os.path.join(self.tmpdir, (fname + ".dat")))
os.remove(os.path.join(self.tmpdir, (fname + ".pyxplot")))
os.remove(os.path.join(self.tmpdir, (fname + ".png")))
os.remove(os.path.join(self.tmpdir, "diagrams.html"))
os.rmdir(self.tmpdir)

View File

@ -0,0 +1,192 @@
#/******************************************************************************
# * Copyright (c)2012 Jan Rheinlaender <jrheinlaender@users.sourceforge.net> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
# * This library is free software; you can redistribute it and/or *
# * modify it under the terms of the GNU Library General Public *
# * License as published by the Free Software Foundation; either *
# * version 2 of the License, or (at your option) any later version. *
# * *
# * This library is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this library; see the file COPYING.LIB. If not, *
# * write to the Free Software Foundation, Inc., 59 Temple Place, *
# * Suite 330, Boston, MA 02111-1307, USA *
# * *
# ******************************************************************************/
import os, subprocess
from PyQt4 import QtCore, QtGui
import FreeCAD, FreeCADGui
header = " ### FREECAD SHAFT WIZARD ###\n #\n"
class Diagram:
function = 0 # This is assumed to be always a SegmentFunction
fname = "y(x)"
xlength = 0.0
xname = "x"
xunit = ""
xscale = 1.0
yname = "y"
yunit = ""
yscale = 1.0
numxpoints = 10
points = []
# process object of pyxplot running in the background
pyxplot = None
# Filesystem watcher
graphicsFile = ""
timer = 0
watcher = 0
updatePending = False
def __init__(self, tmpdir):
self.tmpdir = tmpdir
# Set up watcher and timer
self.watcher = QtCore.QFileSystemWatcher()
self.watcher.fileChanged.connect(self.updateFinished)
self.timer = QtCore.QTimer()
self.timer.setInterval(100)
self.timer.timeout.connect(self.pollFile)
def create(self, title, function, xlength, xname, xunit, xscale, yname, yunit, yscale, numxpoints):
# Initialize
self.title = title
self.function = function
self.xlength = xlength
self.xname = xname
self.xunit = xunit
self.xscale = xscale
self.yname = yname
self.yunit = yunit
self.yscale = yscale
self.numxpoints = numxpoints
self.graphicsFile = os.path.join(self.tmpdir, self.function.name + '.png')
# Calculate points
self.points = self.function.evaluate(self.xlength, self.numxpoints)
# Write files
self.write()
def update(self, function = None, xlength = None):
if function is not None:
self.function = function
if xlength is not None:
self.xlength = xlength
# Calculate points
self.points = self.function.evaluate(self.xlength, self.numxpoints)
# Write files
self.write()
def write(self):
"Write diagram files"
# Check if pyxplot is still running in the background
if (self.pyxplot is not None) and (self.pyxplot.poll() is None):
# Process hasn't terminated yet, set flag to update again
# as soon as the graphicsFile has been written by the current pyxplot process
self.updatePending = True
return
# Get max and min values
(xmin, xmax) = self.minmaxX()
(ymin, ymax) = self.minmaxY()
# Create data file
dataFile = os.path.join(self.tmpdir, (self.function.name + ".dat"))
file = open(dataFile, "w")
file.write(header)
file.write(" # File automatically exported by FreeCAD Shaft Wizard\n")
file.write(" # This file contains xy data, filled with following columns:\n")
file.write(" # 1: %s [%s]\n" % (self.xname, self.xunit))
file.write(" # 2: %s [%s]\n" % (self.yname, self.yunit))
file.write(" #\n")
for (x, y) in self.points:
file.write("%f %f\n" % (x * self.xscale, y * self.yscale))
file.close()
# Create pyxplot file
commandFile = os.path.join(self.tmpdir, (self.function.name + ".pyxplot"))
file = open(commandFile, "w")
file.write(header)
file.write(" # File automatically exported by FreeCAD Shaft Wizard\n")
file.write(" # This file contains a script to plot xy data.\n")
file.write(" # To use it execute:\n")
file.write(" #\n")
file.write(" # pyxplot %s\n" % (commandFile))
file.write(" #\n")
file.write(" #################################################################\n")
# Write general options
file.write("set numeric display latex\n")
file.write("set terminal png\n")
file.write("set output '%s'\n" % (self.graphicsFile))
file.write("set nokey\n")
file.write("set grid\n")
file.write("# X axis\n")
file.write("set xlabel '$%s$ [%s]'\n" % (self.xname, self.xunit))
file.write("set xrange [%f:%f]\n" % ((xmin * self.xscale * 1.05, xmax * self.xscale * 1.05)))
file.write("set xtic\n")
file.write("# Y axis\n")
file.write("set ylabel '$%s$ [%s]'\n" % (self.yname, self.yunit))
file.write("set yrange [%f:%f]\n" % ((ymin * self.yscale * 1.05, ymax * self.yscale * 1.05)))
file.write("set ytic\n")
file.write("# Line styles\n")
file.write("set style 1 line linetype 1 linewidth 2 colour rgb (0):(0):(0)\n")
file.write("# Title\n")
file.write("set title '%s'" % self.title)
# Write plot call
file.write("# Plot\n")
file.write("plot '%s' using 1:2 axes x1y1 with lines style 1\n" % (dataFile))
# Close file
file.close()
# Run pyxplot on the files, but don't wait for execution to finish
# Instead, set timer to poll for the process to finish
try:
self.pyxplot = subprocess.Popen(["pyxplot", commandFile])
# Poll the process to add a watcher as soon as it has created the graphics file
if len(self.watcher.files()) == 0:
self.timer.start()
except OSError:
FreeCAD.Console.PrintError("Can't execute pyxplot. Maybe it is not installed?\n")
self.updatePending = False
def pollFile(self):
# Check if process has finished
if self.pyxplot.poll() is None:
return
# Check if the graphics file has appeared and then set a watcher on it
if not os.path.isfile(self.graphicsFile):
return
# We don't need the timer any more now
self.timer.stop()
FreeCADGui.SendMsgToActiveView('Refresh')
self.watcher.addPath(self.graphicsFile)
def updateFinished(self, filePath):
# filePath is not important because we are only watching a single file
# FIXME: Will give a warning in the console if the active window is not the HTML page
FreeCADGui.SendMsgToActiveView('Refresh')
if self.updatePending is True:
self.write()
def minmaxX(self):
xpoints = []
for (x, y) in self.points:
xpoints.append(x)
return (min(xpoints), max(xpoints))
def minmaxY(self):
ypoints = []
for (x, y) in self.points:
ypoints.append(y)
return (min(ypoints), max(ypoints))

View File

@ -0,0 +1,133 @@
#/******************************************************************************
# * Copyright (c)2012 Jan Rheinlaender <jrheinlaender@users.sourceforge.net> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
# * This library is free software; you can redistribute it and/or *
# * modify it under the terms of the GNU Library General Public *
# * License as published by the Free Software Foundation; either *
# * version 2 of the License, or (at your option) any later version. *
# * *
# * This library is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this library; see the file COPYING.LIB. If not, *
# * write to the Free Software Foundation, Inc., 59 Temple Place, *
# * Suite 330, Boston, MA 02111-1307, USA *
# * *
# ******************************************************************************/
import FreeCAD, FreeCADGui
import Part, Sketcher
class ShaftFeature:
"Creates and updates the feature of the shaft"
Doc = 0
App = FreeCAD
Gui = FreeCADGui
sketch = 0
feature = 0
segments = 0 # number of segments
totalLength = 0 # total length of all segments
lastRadius = 0 # radius of last segment (required for adding segments)
def __init__(self, doc):
"Create new feature"
self.Doc = doc
# TODO: Discover existing sketch and get data from it
self.sketch = self.Doc.addObject("Sketcher::SketchObject","SketchShaft")
self.sketch.Placement = self.App.Placement(self.App.Vector(0,0,0),self.App.Rotation(0,0,0,1))
def addSegment(self, length, diameter):
"Add a segment at the end of the shaft"
# Find constraint indices of vertical line constraint, horizontal line constraint
# FIXME: Should have a unique id instead of indices that might change with user editing
# 0-3 belong to the centerline
# 4-6 to the first vertical section
# 7-9 to the first horizontal section
# 10-12 to the second vertical section
# etc. etc.
constrRadius = 4 + self.segments * 6
constrLength = 7 + self.segments * 6
# Find line index of vertical segment, horizontal segment, last shaft segment
# FIXME: Should have a unique id instead of indices that might change with user editing
segRadius = 1 + self.segments * 2
segLength = 2 + self.segments * 2
prevSegLength = 0 + self.segments * 2
prevSegEnd = 3 + (self.segments - 1) * 2
segEnd = prevSegEnd + 2
radius = diameter / 2
oldLength = self.totalLength
self.totalLength += length
self.segments += 1
if oldLength == 0:
# First segment of shaft
# Create centerline
self.sketch.addGeometry(Part.Line(self.App.Vector(0,0,0), self.App.Vector(self.totalLength,0,0)))
self.sketch.addConstraint(Sketcher.Constraint('DistanceX',0, self.totalLength)) # Constraint1
self.sketch.addConstraint(Sketcher.Constraint('PointOnObject',0,1,-1)) # Constraint2
self.sketch.addConstraint(Sketcher.Constraint('PointOnObject',0,1,-2)) # Constraint3
self.sketch.addConstraint(Sketcher.Constraint('Horizontal', 0)) # Constraint4
# Create first segment
self.sketch.addGeometry(Part.Line(self.App.Vector(0,0,0), self.App.Vector(0,radius,0)))
self.sketch.addConstraint(Sketcher.Constraint('DistanceY',1,radius)) # Constraint5
self.sketch.addConstraint(Sketcher.Constraint('Coincident',0,1,1,1)) # Constraint6
self.sketch.addConstraint(Sketcher.Constraint('Vertical',1)) # Constraint7
self.sketch.addGeometry(Part.Line(self.App.Vector(0,radius,0), self.App.Vector(length,radius,0)))
self.sketch.addConstraint(Sketcher.Constraint('DistanceX',2,length)) # Constraint8
self.sketch.addConstraint(Sketcher.Constraint('Coincident',2,1,1,2)) # Constraint9
self.sketch.addConstraint(Sketcher.Constraint('Horizontal',2)) # Constraint10
else:
# remove line that closes the shaft
self.sketch.delGeometry(prevSegEnd)
# TODO: Delete the two constraints? Or will they disappear automatically?
# Adjust length of centerline
self.sketch.setDatum(0,self.totalLength)
# Add segment at the end
self.sketch.addGeometry(Part.Line(self.App.Vector(oldLength,self.lastRadius,0), self.App.Vector(oldLength,radius,0)))
self.sketch.addConstraint(Sketcher.Constraint('DistanceY', 0, 1, segRadius, 2, radius))
self.sketch.addConstraint(Sketcher.Constraint('Coincident',segRadius,1,prevSegLength,2))
self.sketch.addConstraint(Sketcher.Constraint('Vertical',segRadius))
self.sketch.addGeometry(Part.Line(self.App.Vector(oldLength,radius,0), self.App.Vector(oldLength+length,radius,0)))
self.sketch.addConstraint(Sketcher.Constraint('DistanceX',segLength,length))
self.sketch.addConstraint(Sketcher.Constraint('Coincident',segLength,1,segRadius,2))
self.sketch.addConstraint(Sketcher.Constraint('Horizontal',segLength))
# close the sketch
self.sketch.addGeometry(Part.Line(self.App.Vector(oldLength+length,radius,0), self.App.Vector(oldLength+length,0,0)))
self.sketch.addConstraint(Sketcher.Constraint('Coincident',0,2,segEnd,2))
self.sketch.addConstraint(Sketcher.Constraint('Coincident',segEnd,1,segLength,2))
lastRadius = radius
if oldLength == 0:
# create feature
self.feature = self.Doc.addObject("PartDesign::Revolution","RevolutionShaft")
self.feature.Sketch = self.sketch
self.feature.ReferenceAxis = (self.sketch,['H_Axis'])
self.feature.Angle = 360.0
self.Doc.recompute()
self.Gui.hide("SketchShaft")
else:
self.Doc.recompute()
# FIXME: Will give a warning in the console if the active window is not the feature
self.Gui.SendMsgToActiveView("ViewFit")
def updateSegment(self, segment, oldLength, length, diameter):
constrRadius = 4 + segment * 6
constrLength = 7 + segment * 6
# update total length
self.totalLength = self.totalLength - oldLength + length
# Adjust length of centerline
self.sketch.setDatum(0,self.totalLength)
# Adjust segment length
self.sketch.setDatum(constrLength, length)
self.sketch.setDatum(constrRadius, diameter/2)
# Update feature
self.Doc.recompute()
self.Gui.SendMsgToActiveView("ViewFit")

View File

@ -0,0 +1,118 @@
#/******************************************************************************
# * Copyright (c)2012 Jan Rheinlaender <jrheinlaender@users.sourceforge.net> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
# * This library is free software; you can redistribute it and/or *
# * modify it under the terms of the GNU Library General Public *
# * License as published by the Free Software Foundation; either *
# * version 2 of the License, or (at your option) any later version. *
# * *
# * This library is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this library; see the file COPYING.LIB. If not, *
# * write to the Free Software Foundation, Inc., 59 Temple Place, *
# * Suite 330, Boston, MA 02111-1307, USA *
# * *
# ******************************************************************************/
import FreeCAD, FreeCADGui
import os
from PyQt4 import QtCore, QtGui
from WizardShaftTable import WizardShaftTable
from Shaft import Shaft
class WizardShaft:
"Shaft Wizard"
# GUI
App = FreeCAD
Gui = FreeCADGui
doc = Gui.ActiveDocument
table = 0
# Shaft
shaft = 0
# Feature
sketch = 0
featureWindow = 0
def __init__(self):
#FreeCADGui.activeWorkbench()
#
"Initialize the user interface"
### Create a dock window
wizardWidget = QtGui.QDockWidget("Shaft Wizard")
wizardWidget.setObjectName("Shaft Wizard")
mw = QtGui.qApp.activeWindow()
mw.addDockWidget(QtCore.Qt.TopDockWidgetArea,wizardWidget)
cw = mw.centralWidget() # This is a qmdiarea widget
# Get active document or create a new one
# Important because when setting the default data in WizardShaftTable() the
# updateSketch() slot will be activated and it relies on finding a valid document
if self.doc == None:
self.Gui.activateWorkbench("PartDesignWorkbench")
self.doc = self.App.newDocument()
# Grab the newly created feature window
featureWindow = cw.subWindowList()[-1]
else:
featureWindow = cw.activeSubWindow()
# Create Shaft object
self.shaft = Shaft(self.doc)
#self.shaft.featureWindow = featureWindow
# Assign a table widget to the dock window
self.table = WizardShaftTable(self, self.shaft)
wizardWidget.setWidget(self.table.widget)
# Grab the newly created HTML window
cw = mw.centralWidget()
htmlWindow = cw.subWindowList()[-1]
#self.shaft.htmlWindow = htmlWindow
def updateEdge(self, column, start):
print "Not implemented yet - waiting for robust references..."
return
if self.sketchClosed is not True:
return
# Create a chamfer or fillet at the start or end edge of the segment
if start is True:
row = rowStartEdgeType
idx = 0
else:
row = rowEndEdgeType
idx = 1
edgeType = self.tableWidget.item(row, column).text().toAscii()[0].upper()
if not ((edgeType == "C") or (edgeType == "F")):
return # neither chamfer nor fillet defined
if edgeType == "C":
objName = self.doc.addObject("PartDesign::Chamfer","ChamferShaft%u" % (column * 2 + idx))
else:
objName = self.doc.addObject("PartDesign::Fillet","FilletShaft%u" % (column * 2 + idx))
if objName == "":
return
edgeName = "Edge%u" % self.getEdgeIndex(column, idx, edgeType)
self.doc.getObject(objName).Base = (self.doc.getObject("RevolutionShaft"),"[%s]" % edgeName)
# etc. etc.
def getEdgeIndex(self, column, startIdx):
# FIXME: This is impossible without robust references anchored in the sketch!!!
return
class WizardShaftGui:
def Activated(self):
WizardShaft()
def GetResources(self):
IconPath = FreeCAD.ConfigGet("AppHomePath") + "Mod/PartDesign/Gui/Resources/icons/WizardShaftIcon.png"
MenuText = 'Start the shaft design wizard'
ToolTip = 'Start the shaft design wizard'
return {'Pixmap' : IconPath, 'MenuText': MenuText, 'ToolTip': ToolTip}
FreeCADGui.addCommand('PartDesign_WizardShaft', WizardShaftGui())

View File

@ -0,0 +1,332 @@
#/******************************************************************************
# * Copyright (c)2012 Jan Rheinlaender <jrheinlaender@users.sourceforge.net> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
# * This library is free software; you can redistribute it and/or *
# * modify it under the terms of the GNU Library General Public *
# * License as published by the Free Software Foundation; either *
# * version 2 of the License, or (at your option) any later version. *
# * *
# * This library is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this library; see the file COPYING.LIB. If not, *
# * write to the Free Software Foundation, Inc., 59 Temple Place, *
# * Suite 330, Boston, MA 02111-1307, USA *
# * *
# ******************************************************************************/
from PyQt4 import QtCore, QtGui
class WizardShaftTable:
"The table widget that contains all the data of the shaft"
# Dictionary to access different parameters without using a numeric row index that might change
# as the code evolves
rowDict = {
"Length" : 0,
"Diameter" : 1,
"LoadType" : 2,
"LoadSize" : 3,
"LoadLocation" : 4,
"StartEdgeType" : 5,
"StartEdgeSize" : 6,
"EndEdgeType" : 7,
"EndEdgeSize" : 8
}
rowDictReverse = {}
headers = ["Length [mm]",
"Diameter [mm]",
"Load type",
"Load [N]",
"Location [mm]",
"Start edge type",
"Start edge size",
"End edge type",
"End edge size"
]
widget = 0
wizard = 0
shaft = 0
def __init__(self, w, s):
for key in self.rowDict.iterkeys():
self.rowDictReverse[self.rowDict[key]] = key
# Set parent wizard (for connecting slots)
self.wizard = w
self.shaft = s
# Create table widget
self.widget = QtGui.QTableWidget(len(self.rowDict), 0)
self.widget.resize(QtCore.QSize(300,100))
#self.widget.setFocusPolicy(QtCore.Qt.StrongFocus)
# Label rows and columns
self.widget.setVerticalHeaderLabels(self.headers)
self.widget.setHorizontalHeaderLabels(["Section 1", "Section 2"])
#self.widget.columnMoved.connect(column, oldIndex, newIndex)
# Create context menu
action = QtGui.QAction("Add column", self.widget)
action.triggered.connect(self.slotInsertColumn)
self.widget.addAction(action)
self.widget.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
# Set some default data
# Section 1
self.addColumn()
self.setLength(0, 40.0)
self.setDiameter(0, 50.0)
self.setLoadType(0, "Static")
self.setLoadSize(0, 1000.0)
self.setLoadLocation(0, 25.0)
# Section 2
self.addColumn()
self.setLength(1, 80.0)
self.setDiameter(1, 60.0)
self.setLoadType(1, "Fixed")
def slotInsertColumn(self, point):
# FIXME: Allow inserting columns, not just adding at the end
# Note: need to re-name all the following column headers then
# if (column == self.tableWidget.columnCount()):
self.addColumn()
# else:
# self.insertColumn(index)
def addColumn(self):
"Add a new column, fill it with widgets, and connect the signals"
index = self.widget.columnCount()
# Make an intelligent guess at the length/dia of the next segment
if index > 0:
length = self.shaft.segments[index-1].length
diameter = self.shaft.segments[index-1].diameter
if index > 2:
diameter -= 5.0
else:
diameter += 5.0
else:
length = 20.0
diameter = 10.0
self.shaft.addSegment(length, diameter)
self.widget.insertColumn(index)
self.widget.setHorizontalHeaderItem(index + 1, QtGui.QTableWidgetItem("Section %s" % (index + 1)))
# Length
widget = QtGui.QDoubleSpinBox(self.widget)
widget.setMinimum(0)
widget.setMaximum(1E9)
self.widget.setCellWidget(self.rowDict["Length"],index, widget)
widget.setValue(length)
widget.valueChanged.connect(self.slotValueChanged)
widget.editingFinished.connect(self.slotEditingFinished)
# Diameter
widget = QtGui.QDoubleSpinBox(self.widget)
widget.setMinimum(0)
widget.setMaximum(1E9)
self.widget.setCellWidget(self.rowDict["Diameter"],index, widget)
widget.setValue(diameter)
widget.valueChanged.connect(self.slotValueChanged)
widget.editingFinished.connect(self.slotEditingFinished)
# Load type
widget = QtGui.QListWidget(self.widget)
widget.insertItem(0, QtGui.QListWidgetItem("None", widget))
widget.insertItem(1, QtGui.QListWidgetItem("Fixed", widget))
widget.insertItem(2, QtGui.QListWidgetItem("Static", widget))
widget.insertItem(3, QtGui.QListWidgetItem("Bearing", widget))
widget.insertItem(4, QtGui.QListWidgetItem("Pulley", widget))
self.widget.setCellWidget(self.rowDict["LoadType"],index, widget)
widget.setCurrentRow(0)
widget.currentItemChanged.connect(self.slotLoadType)
# Load size
widget = QtGui.QDoubleSpinBox(self.widget)
widget.setMinimum(-1E9)
widget.setMaximum(1E9)
self.widget.setCellWidget(self.rowDict["LoadSize"],index, widget)
widget.setValue(0)
widget.valueChanged.connect(self.slotValueChanged)
widget.editingFinished.connect(self.slotEditingFinished)
# Load location
widget = QtGui.QDoubleSpinBox(self.widget)
widget.setMinimum(0)
widget.setMaximum(1E9)
self.widget.setCellWidget(self.rowDict["LoadLocation"],index, widget)
widget.setValue(0)
widget.valueChanged.connect(self.slotValueChanged)
widget.editingFinished.connect(self.slotEditingFinished)
# Start edge type
widget = QtGui.QListWidget(self.widget)
widget.insertItem(0, QtGui.QListWidgetItem("None", widget))
widget.insertItem(1, QtGui.QListWidgetItem("Chamfer", widget))
widget.insertItem(2, QtGui.QListWidgetItem("Fillet", widget))
self.widget.setCellWidget(self.rowDict["StartEdgeType"],index, widget)
widget.setCurrentRow(0)
widget.currentItemChanged.connect(self.slotStartEdgeType)
# Start edge size
widget = QtGui.QDoubleSpinBox(self.widget)
widget.setMinimum(0)
widget.setMaximum(1E9)
self.widget.setCellWidget(self.rowDict["StartEdgeSize"],index, widget)
widget.setValue(1)
widget.valueChanged.connect(self.slotValueChanged)
widget.editingFinished.connect(self.slotEditingFinished)
# End edge type
widget = QtGui.QListWidget(self.widget)
widget.insertItem(0, QtGui.QListWidgetItem("None", widget))
widget.insertItem(1, QtGui.QListWidgetItem("Chamfer", widget))
widget.insertItem(2, QtGui.QListWidgetItem("Fillet", widget))
self.widget.setCellWidget(self.rowDict["EndEdgeType"],index, widget)
widget.setCurrentRow(0)
widget.currentItemChanged.connect(self.slotEndEdgeType)
# End edge size
widget = QtGui.QDoubleSpinBox(self.widget)
widget.setMinimum(0)
widget.setMaximum(1E9)
self.widget.setCellWidget(self.rowDict["EndEdgeSize"],index, widget)
widget.setValue(1)
widget.valueChanged.connect(self.slotValueChanged)
widget.editingFinished.connect(self.slotEditingFinished)
def slotValueChanged(self, value):
(self.editedRow, self.editedColumn) = self.getFocusedCell()
self.editedValue = value
def slotEditingFinished(self):
rowName = self.rowDictReverse[self.editedRow]
if rowName is None:
return
if rowName == "Length":
self.shaft.updateSegment(self.editedColumn, length = self.getDoubleValue(rowName, self.editedColumn))
elif rowName == "Diameter":
self.shaft.updateSegment(self.editedColumn, diameter = self.getDoubleValue(rowName, self.editedColumn))
elif rowName == "LoadType":
self.shaft.updateLoad(self.editedColumn, loadType = self.getListValue(rowName, self.editedColumn))
elif rowName == "LoadSize":
self.shaft.updateLoad(self.editedColumn, loadSize = self.getDoubleValue(rowName, self.editedColumn))
elif rowName == "LoadLocation":
self.shaft.updateLoad(self.editedColumn, loadLocation = self.getDoubleValue(rowName, self.editedColumn))
elif rowName == "StartEdgeType":
pass
elif rowName == "StartEdgeSize":
pass
elif rowName == "EndEdgeType":
pass
elif rowName == "EndEdgeSize":
pass
def setLength(self, column, l):
self.setDoubleValue("Length", column, l)
self.shaft.updateSegment(column, length = l)
def getLength(self, column):
return self.getDoubleValue("Length", column)
def setDiameter(self, column, d):
self.setDoubleValue("Diameter", column, d)
self.shaft.updateSegment(column, diameter = d)
def getDiameter(self, column):
return self.getDoubleValue("Diameter", column)
def slotLoadType(self, new, old):
if new.text() != "Fixed":
if (self.getLoadSize is None) or (self.getLoadLocation is None):
return
self.shaft.updateLoad(self.getFocusedColumn(), loadType = new.text())
def setLoadType(self, column, t):
self.setListValue("LoadType", column, t)
self.shaft.updateLoad(column, loadType = t)
def getLoadType(self, column):
return self.getListValue("LoadType", column)
def setLoadSize(self, column, s):
self.setDoubleValue("LoadSize", column, s)
self.shaft.updateLoad(column, loadSize = s)
def getLoadSize(self, column):
return self.getDoubleValue("LoadSize", column)
def setLoadLocation(self, column, l):
self.setDoubleValue("LoadLocation", column, l)
self.shaft.updateLoad(column, loadLocation = l)
def getLoadLocation(self, column):
return self.getDoubleValue("LoadLocation", column)
def slotStartEdgeType(self, old, new):
pass
def setStartEdgeType(self, column, t):
self.setListValue("StartEdgeType", column, t)
def getStartEdgeType(self, column):
return self.getListValue("StartEdgeType", column)
def setStartEdgeSize(self, column, s):
self.setDoubleValue("StartEdgeSize", column, s)
def getStartEdgeSize(self, column):
return self.getDoubleValue("StartEdgeSize", column)
def slotEndEdgeType(self, old, new):
pass
def setEndEdgeType(self, column, t):
self.setListValue("EndEdgeType", column, t)
def getEndEdgeType(self, column):
return self.getListValue("EndEdgeType", column)
def setEndEdgeSize(self, column, s):
self.setDoubleValue("EndEdgeSize", column, s)
def getEndEdgeSize(self, column):
return self.getDoubleValue("EndEdgeSize", column)
def setDoubleValue(self, row, column, v):
widget = self.widget.cellWidget(self.rowDict[row], column)
# Avoid triggering a signal, because the slot will work on the focused cell, not the current one
widget.blockSignals(True)
widget.setValue(v)
widget.blockSignals(False)
def getDoubleValue(self, row, column):
widget = self.widget.cellWidget(self.rowDict[row], column)
if widget is not None:
return widget.value()
else:
return None
def setListValue(self, row, column, v):
widget = self.widget.cellWidget(self.rowDict[row], column)
widget.blockSignals(True)
widget.setCurrentItem(widget.findItems(v, QtCore.Qt.MatchExactly)[0])
widget.blockSignals(False)
def getListValue(self, row, column):
widget = self.widget.cellWidget(self.rowDict[row], column)
if widget is not None:
return widget.currentItem().text().toAscii()[0].upper()
else:
return None
def getFocusedColumn(self):
# Make the focused cell also the current one in the table
widget = QtGui.QApplication.focusWidget()
if widget is not None:
index = self.widget.indexAt(widget.pos())
self.widget.setCurrentCell(index.row(), index.column())
return self.widget.currentColumn()
def getFocusedCell(self):
# Make the focused cell also the current one in the table
widget = QtGui.QApplication.focusWidget()
if widget is not None:
index = self.widget.indexAt(widget.pos())
self.widget.setCurrentCell(index.row(), index.column())
return (self.widget.currentRow(), self.widget.currentColumn())

View File

@ -0,0 +1,27 @@
"""
Shaft Wizard
"""
#/******************************************************************************
# * Copyright (c)2012 Jan Rheinlaender <jrheinlaender@users.sourceforge.net> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
# * This library is free software; you can redistribute it and/or *
# * modify it under the terms of the GNU Library General Public *
# * License as published by the Free Software Foundation; either *
# * version 2 of the License, or (at your option) any later version. *
# * *
# * This library is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this library; see the file COPYING.LIB. If not, *
# * write to the Free Software Foundation, Inc., 59 Temple Place, *
# * Suite 330, Boston, MA 02111-1307, USA *
# * *
# ******************************************************************************/
# Empty file to treat the folder as a package