INTRODUCTION
As a learning exercise in FreeCAD modelling I decided to remodel the design for a pair of vertical steam engine models. This design, done during 2011 using a well known non-parametric commercial CAD tool, was based on a single cylinder model my twin brother and I designed (with no design tools!) and, under paternal supervision in our home workshop, built in 1956 when we were 14 years old.
The design allows two configurations: one with a single cylinder and one with two cylinders. Almost all part types are common; only three part types depend on the configuration (base, crank shaft, cylinder platform) and of course the single cylinder configuration doesn't use the inlet and exhaust manifolds. The design does not forsee the usual mechanism with a lever and twin eccentrics to invert the direction of rotation of the crank shaft. Even without this option, I felt that I'd probably bitten off more than I could che[*]w! The photo shows the dual cylinder version of the engine which I built during 2012.
Two important objectives for this remodelling project were:
- add a reversing mechanism option
- structure the models so that the two key FreeCAD novelties (for me), namely the parametric capability and the Phython macro capability, could be used to provide animations.
There are two aspects of the modelling approach I took which may be of interest to some readers although I'm sure its all been done before. The first relates to handling the complexity of the models, the second relates to the animation of the somewhat complex movements of the reversing link mechanism.
HANDLING THE COMPLEXITY
SubParts and Parts
The design with the reversing link option comprises over seventy distinct types of part and subpart not counting the nots, bolts and screws. By subpart I mean an item which exists briefly during the fabrication process but which, together with other subparts, form a single type of part by getting joined by an irreversable process such as soldering, welding, glueing or rivetting. So one aspect of complexity derives simply from the large number of distinct shapes which must be modelled and which (not yet done) must appear on readable design documents showing the appropriate projections, dimensions and instructions to favour smooth and reliable fabrication in the home workshop.
As a start to handling this aspect of complexity I divided the parts and subparts into groups of related parts and modelled each such group in a separate FreeCAD file. Within each file I used the FreeCAD Group facility to separate Subparts and Parts. Within each of these Groups, the members are allocated placements in which the Y coordinate is always 0 but the X and Z coordinates increase by an appropriate amount to ensure that the six standard views (top, front, back ..) never show overlap between adjacent members even when all members of the group are Visible.
Sub-Assemblies
A second aspect of complexity relates to modeling sub-assemblies of Parts and the overall assemblies of sub-assemblies. Like all other FreeCAD fans, I cant wait to use the coming Assembly Tool; in the meantime I decided to be pragmatic, ie to build up my sub-assemblies by using Simple Copies of the original parametric designs of the parts. So to get a part to participate in a sub-assembly I open the file containing the Part, make a simple copy thereof in the Part file, Copy the simple copy to the clip-board, close the Part File without saving, and finally paste the simple copy into the sub-assembly file and adjust its Placement parameters to get the desired fit into the sub-assembly.
The non-moving parts are divided into three sub-assemblies held in distinct files the Base Assy, the Vertical Structure Assy, and the Cylinder Assy. Within each of these files there are two versions in distinct FreeCAD Groups - a mono version and a dual version.
The sub-assemblies for the moving parts were divided between two FreeCAD files - one for the configurations without the reversing option and one for the Reversing Option sub-assemblies. Each sub-assembly is held in a distinct FreeCAD Group. There is one such sub-assembly for each group of parts which are rigidly held together and which move as a whole.
Examples are the Dual Crank Shaft Assy, the Eccentric Strap Assy, The ConRod Assy etc. The Parts in the Dual Crank Shaft Assy are one Dual Crank Shaft, one Flywheel, one Flywheel grubscrew, two Eccentrics and two Eccentric grubscrews. To facilitate the animation of a single moving assembly, it is important to place the reference point of the subassembly at the coordinate origin and to orient the individual parts correctly. Then the final step for modelling a moving sub-assembly is to unify all the parts as a FreeCAD Compound with its reference point at the origin and with a rotational angle of zero. This precaution allows the animation of the entire sub-assembly to be controlled by setting the Placement parameters of the Compound.
Overall Assemblies
The two overall assemblies presented here are held in distinct FreeCAD groups in a single file. The choice of the engine to animate is made by enabling the visibility of one of the two. The file also contains a copy of the conceptual sketch of the reversing mechanism which, as explained above, is used by the animation macro.
ANIMATION TECHNIQUE
Introduction
During an animation of the motion of an engine, the independent variable is of course the rotational angle of the crank shaft about the X axis. The animation simply steps this angle cyclically by a suitable small increment, and must compute and set the consequent Placements for all the moving sub-assemblies. Animation of the Crank Shaft Assy, the ConRod Assy and the Piston Assy is relatively easy because the positions of the reference points and the rotational angles can be readily calculated using simple and well-known trig formulae. But calculating the placements of the sub-assemblies of the reversing mechanism is significantly more complex and proved to be beyond my limited math!
As a first step in designing this mechanism I had used the FreeCAD Sketcher to construct a conceptual model of the mechanism and found to my joy that Sketcher solved in a jiffy the math I had found too tough, and also that I could also animate the Sketch via a very simple macro which cyclically steps the value of the Constraint which models the rotational angle of the eccentics. So I had the idea of using this Sketch in the3D animation macro to extract the appropriate placements for the moving sub-assemblies of the 3D model. I tried this out and it worked fine.
The Reversing Mechanism Sketch
Below is a screen shot of this conceptual Sketch; with an image editor I have added identifying letters for the line ends. These identifiers are used in the 3D animation macro as part of the names of the variables used to hold the coordinates of the points extracted from the sketch.
Here is the short macro which animates this sketch.
Code: Select all
#Macro Begin: AnimateReversingLinkSketch
import FreeCAD
import time
sketch = App.ActiveDocument.Sketch
for rev in range(0,10):
for theta in range(90,451,10):
sketch.setDatum(39,App.Units.Quantity(str(theta) + ' deg'))
time.sleep(0.02)
#Macro End:
- OA and OB represent the offset axes of the two eccentrics. AOB always forms a straight line.
- AC and BD represent the two eccentric straps.
- EF represents the center line of the link's slot while C, D and G represent the centers of the three holes in this link about which the eccentric straps (AC and BD) and the reversing link strap GH can freely swivel.
- KL represents the valve assembly; it is constrained to lie on the sketches X axis and its end L is constrained to lie on the line EF.
- HI and IJ represent respectively the reversing arm and the reversing lever; the angle between these is constrained to be 90 deg. To run forewards (backwards) the angle between HI and the horizontal must be +14 (-14) deg. The sketch is shown in the position for going in reverse; in this position the horizontal movement of the valve is in effect determined by the eccentric OA.
- The lengths of all lines are fixed by constraints
Here is a single macro of about 300 lines which animates both the mono and the dual cylinder configurations. To use it the Report View and the Python Console must be visible and both Python errors and Python internal output should be redirected to the Report View.
Code: Select all
# Macro Begin: AnimateReversingEngines
import FreeCAD
import FreeCADGui
from PySide import QtCore
from math import sqrt, pi, sin, cos, asin,atan
import string
def trace(s):
"""Trace routine for debugging"""
#print s
def askWhatToDo():
"""Asks the operator what to do next and sets up the action to obey"""
global mono, timer, timerIsConnected, StopAnimation, state, direction, periodMilliseconds
global theta, step, stepsPerRev, armAngle0Deg, armAngleDeg, Z0
if timerIsConnected:
timer.stop()
timer.timeout.disconnect() # Stop timeout events
timerIsConnected = False
if direction != "F" and direction != "B":
print "Hello!"
reply = "D"
else:
reply = "?"
# Loop acquires a valid reply to the question
while True:
if reply == "?":
print "On Python console, please enter D (Direction change), R (Run) or Q (Quit), followed by Enter"
reply = raw_input();
ok = (len(reply) == 1)
if ok:
reply = string.upper(reply[0])
if reply == 'Q':
print "Bye\n"
return
# Check that exactly one OverallAsy is visible
mono = FreeCADGui.ActiveDocument.getObject("Group006").Visibility
dual = FreeCADGui.ActiveDocument.getObject("Group003").Visibility
if (mono and dual) or not(mono or dual):
print "Either Group - Overall1Assy or Group - Overall2Assy, but not both, must be visible!"
ok = True
elif reply == 'D' or reply == 'R':
break
if not ok:
print "Bad reply! Try again please."
reply = "?"
# Reply is D or R
if mono:
SetupMonoEngine()
else:
SetupDualEngine()
print "Done Engine setup"
if reply == "D":
if direction == "F":
direction = "B"
else:
direction = "F"
# Acquire current angle in Sketch of EccsArm
sketch = skDoc.Sketch
skLinkArmHtoI =sketch.Geometry[10]
Hy = skLinkArmHtoI.StartPoint.y
Hz = Z0 - skLinkArmHtoI.StartPoint.x
Iy = skLinkArmHtoI.EndPoint.y
Iz = Z0 - skLinkArmHtoI.EndPoint.x
armAngle0Deg = -atan((Hy-Iy)/(Hz-Iz)) / fac
if direction == 'F':
armAngleDeg = armAngleFDeg
else:
armAngleDeg = armAngleBDeg
# Start animation of changing direction
print "Starting animation of direction change"
timerIsConnected = True
step = 1
timer.timeout.connect(setDirection)
timer.start(periodMilliseconds)
else:
# Start animation of running
print "Starting animation of running the engine. Press ENTER to stop animation at end of current rotation."
StopAnimation = False
timerIsConnected = True
theta = 0
step = stepsPerRev
timer.timeout.connect(runEngine)
timer.start(periodMilliseconds)
reply = raw_input()
StopAnimation = True
def PistonAnimationStep(theta, X, ConRodAssy, PistonAssy):
""" Performs one animation step for a ConRodAssy and a PistonAssy """
global fac, CrankThrow, ConRodLength, Z0
thetaRad = theta * fac
YY = -CrankThrow * sin(thetaRad)
ZZ = CrankThrow * cos(thetaRad) + Z0
psiRad = asin(YY/ConRodLength)
psiDeg = psiRad / fac
ConRodAssy.Placement = App.Placement(App.Vector(X,YY,ZZ), App.Rotation(App.Vector(1,0,0), psiDeg))
ZZ = ZZ + ConRodLength * cos(psiRad)
PistonAssy.Placement = App.Placement(App.Vector(X,0,ZZ), App.Rotation(App.Vector(0,0,1), 0))
def ReversingAssyAnimationStep(theta, alpha, X, Eccs, ERodA, ERodB, ELink, ESlipper, EStrap, EValve, ELever):
""" Performs one animation step for the moving parts of a reversing link assembly"""
global skDoc, Z0
# Set angle for eccentics and for reversing lever in Sketch and recalculate sketch geometry
sketch = skDoc.Sketch
sketch.setDatum(39, App.Units.Quantity(str(theta + 90) + ' deg'))
sketch.setDatum(37, App.Units.Quantity(str(alpha) + ' deg'))
skDoc.recompute()
# Extract GeomLineSegment objects from Sketch
skCrankOtoB = sketch.Geometry[0]
skCrankOtoA = sketch.Geometry[1]
skRodAtoC = sketch.Geometry[2]
skRodBtoD = sketch.Geometry[3]
skLinkLobeCtoE = sketch.Geometry[5]
skLinkLobeDtoF = sketch.Geometry[6]
skLinkEtoF = sketch.Geometry[7]
skLinkFtoG = sketch.Geometry[8]
skLinkStrapGtoH = sketch.Geometry[9]
skLinkArmHtoI =sketch.Geometry[10]
skValveRodKtoL = sketch.Geometry[14]
skControlLeverItoJ = sketch.Geometry[15]
# Extract point coordinates from Sketch lines and transform from sketch coordinates to model coordinates
# Transformation is as follows: Ym = Ys; Zm = Z0 - Xs
Ay = skCrankOtoA.EndPoint.y
Az = Z0 - skCrankOtoA.EndPoint.x
trace("Ay=" + str(Ay) + ", Az=" + str(Az) + "\n")
By = skCrankOtoB.EndPoint.y
Bz = Z0 - skCrankOtoB.EndPoint.x
trace("By=" + str(By) + ", Bz=" + str(Bz) + "\n")
Cy = skRodAtoC.EndPoint.y
Cz = Z0 - skRodAtoC.EndPoint.x
trace("Cy=" + str(Cy) + ", Cz=" + str(Cz) + "\n")
Dy = skRodBtoD.EndPoint.y
Dz = Z0 - skRodBtoD.EndPoint.x
trace("Dy=" + str(Dy) + ", Dz=" + str(Dz) + "\n")
Ey = skLinkLobeCtoE.EndPoint.y
Ez = Z0 - skLinkLobeCtoE.EndPoint.x
trace("Ey=" + str(Ey) + ", Ez=" + str(Ez) + "\n")
Gy = skLinkFtoG.EndPoint.y
Gz = Z0 - skLinkFtoG.EndPoint.x
trace("Gy=" + str(Gy) + ", Gz=" + str(Gz) + "\n")
Hy = skLinkStrapGtoH.EndPoint.y
Hz = Z0 - skLinkStrapGtoH.EndPoint.x
trace("Hy=" + str(Hy) + ", Hz=" + str(Hz) + "\n")
Iy = skLinkArmHtoI.EndPoint.y
Iz = Z0 - skLinkArmHtoI.EndPoint.x
trace("Iy=" + str(Iy) + ", Iz=" + str(Iz) + "\n")
Jy = skControlLeverItoJ.EndPoint.y
Jz = Z0 - skControlLeverItoJ.EndPoint.x
trace("Jy=" + str(Jy) + ", Jz=" + str(Jz) + "\n")
Ly = skValveRodKtoL.EndPoint.y
Lz = Z0 - skValveRodKtoL.EndPoint.x
trace("Ly=" + str(Ly) + ", Lz=" + str(Lz) + "\n")
Eccs.Placement = App.Placement(App.Vector(X,0,Z0), App.Rotation(App.Vector(1,0,0), theta))
psiDeg = atan((Cy-Ay)/(Cz-Az)) / fac
ERodA.Placement = App.Placement(App.Vector(X,Ay,Az),App.Rotation(App.Vector(1,0,0),-psiDeg))
psiDeg = atan((Dy-By)/(Dz-Bz)) / fac
ERodB.Placement = App.Placement(App.Vector(X,By,Bz),App.Rotation(App.Vector(1,0,0),-psiDeg))
psiDeg = atan((Cz-Dz)/(Cy-Dy)) / fac
ELink.Placement = App.Placement(App.Vector(X,Dy,Dz),App.Rotation(App.Vector(1,0,0),psiDeg))
ESlipper.Placement = App.Placement(App.Vector(X,Ly,Lz),App.Rotation(App.Vector(1,0,0),psiDeg))
psiDeg = atan((Hz-Gz)/(Hy-Gy)) / fac
EStrap.Placement = App.Placement(App.Vector(X,Gy,Gz),App.Rotation(App.Vector(1,0,0),psiDeg))
psiDeg = atan((Hy-Iy)/(Hz-Iz)) / fac
ELever.Placement = App.Placement(App.Vector(0,Iy,Iz),App.Rotation(App.Vector(1,0,0),-psiDeg))
EValve.Placement = App.Placement(App.Vector(X,Ly,Lz), App.Rotation(App.Vector(0,0,1), 0))
def animationStep(mono):
"""Modifies Placements of all movable objects of model using data extracted from Sketch"""
global theta, armAngle0Deg, Z0, EccsXR, pistonXR
global shCrankAssy, shEccsLeverAssy
global shConRodAssyR, shPistonAssyR, shEccentricsR, shEccRodAssyRA, shEccRodAssyRB
global shEccsLinkR, shEccsLinkStrapAssyR, shEccsLinkSlipperR, shEccsValveAssyR
global shConRodAssyL, shPistonAssyL, shEccentricsL, shEccRodAssyLA, shEccRodAssyLB
global shEccsLinkL, shEccsLinkStrapAssyL, shEccsLinkSlipperL, shEccsValveAssyL
# Modify placements of 3D moving objects
shCrankAssy.Placement = App.Placement(App.Vector(0,0,Z0), App.Rotation(App.Vector(1,0,0), theta+90))
if mono:
PistonAnimationStep(theta, pistonXR, shConRodAssyR, shPistonAssyR)
ReversingAssyAnimationStep(theta, armAngle0Deg, EccsXR, shEccentricsR, shEccRodAssyRA, shEccRodAssyRB,\
shEccsLinkR, shEccsLinkSlipperR, shEccsLinkStrapAssyR, shEccsValveAssyR, shEccsLeverAssy)
else:
PistonAnimationStep(theta+90, pistonXR, shConRodAssyR, shPistonAssyR)
PistonAnimationStep(theta, -pistonXR, shConRodAssyL, shPistonAssyL)
ReversingAssyAnimationStep(theta+90, armAngle0Deg, EccsXR, shEccentricsR, shEccRodAssyRA, shEccRodAssyRB,\
shEccsLinkR, shEccsLinkSlipperR, shEccsLinkStrapAssyR, shEccsValveAssyR, shEccsLeverAssy)
ReversingAssyAnimationStep(theta, armAngle0Deg, -EccsXR, shEccentricsL, shEccRodAssyLA, shEccRodAssyLB,\
shEccsLinkL, shEccsLinkSlipperL, shEccsLinkStrapAssyL, shEccsValveAssyL, shEccsLeverAssy)
# Update display
Gui.updateGui()
def setDirection():
"""Called by timer. Controls the animation of setting the link according to the required direction"""
global mono, armAngle0Deg, armAngleDeg, step
if armAngleDeg != armAngle0Deg:
# Compute new angle for this step
if armAngle0Deg < armAngleDeg:
armAngle0Deg = armAngle0Deg + step
if armAngle0Deg > armAngleDeg:
armAngle0Deg = armAngleDeg
else:
armAngle0Deg = armAngle0Deg - step
if armAngle0Deg < armAngleDeg:
armAngle0Deg = armAngleDeg
# Do the animation step
animationStep(mono)
else:
# Final position has been reached, so stop this and ask what to do next
askWhatToDo()
def runEngine():
"""Called by timer. Controls the animation of running the engine"""
global mono, StopAnimation, theta, direction, stepsPerRev, stepSizeDeg, step
step = step - 1
if step < 0: # Check for end of turn
theta = 0
step = stepsPerRev
if StopAnimation:
askWhatToDo()
return
animationStep(mono)
if direction == 'F':
theta = theta + stepSizeDeg
else:
theta = theta - stepSizeDeg
def SetupMonoEngine():
# Define geometry constants
global EccsXR, PillarXR, pistonXR
pistonXR = 0 # X corrdinate of piston axis for mono cylinder engine
EccsXR = 26 # X coodinate of eccentric assembly for mono cylinder engine
PillarXR = 12 # X coordinate of right pillar for mono cylinder engine. Left pillar is at -PillarZR
# Define variables for 3D shapes of mono cylinder model to animate or position
global shCrankAssy, shConRodAssyR, shPistonAssyR, shEccentricsR, shEccRodAssyRA, shEccRodAssyRB
global shEccsLinkR, shEccsLinkStrapAssyR, shEccsLeverAssy, shEccsLinkSlipperR, shEccsValveAssyR
global shCrabR, shCrabL
shCrankAssy = assyDoc.getObject("Compound010")
shConRodAssyR = assyDoc.getObject("Compound011")
shPistonAssyR = assyDoc.getObject("Compound012")
shEccentricsR = assyDoc.getObject("Cut001003001")
shEccRodAssyRA = assyDoc.getObject("Compound")
shEccRodAssyRB = assyDoc.getObject("Compound001")
shEccsLinkR = assyDoc.getObject("Cut001005001")
shEccsLinkStrapAssyR = assyDoc.getObject("Compound002")
shEccsLeverAssy = assyDoc.getObject("Compound042")
shEccsLinkSlipperR = assyDoc.getObject("Cut001005002001")
shEccsValveAssyR = assyDoc.getObject("Compound003")
def SetupDualEngine():
# Define geometry constants
global EccsXR, PillarXR, pistonXR
pistonXR = 16.5 # X corrdinate of piston axis for dual cylinder engine
EccsXR = 42.5 # X coodinate of eccentric assembly for dual cylinder engine
PillarXR = 28.5 # X coordinate of right pillar for dual cylinder engine. Left pillar is at -PillarZR
# Define variables for 3D shapes of dual cylinder model to animate or position
global shCrankAssy, shEccsLeverAssy
global shConRodAssyR, shPistonAssyR, shEccentricsR, shEccRodAssyRA, shEccRodAssyRB
global shEccsLinkR, shEccsLinkStrapAssyR, shEccsLinkSlipperR, shEccsValveAssyR
global shConRodAssyL, shPistonAssyL, shEccentricsL, shEccRodAssyLA, shEccRodAssyLB
global shEccsLinkL, shEccsLinkStrapAssyL, shEccsLinkSlipperL, shEccsValveAssyL
shCrankAssy = assyDoc.getObject("Fusion033003002024")
shEccsLeverAssy = assyDoc.getObject("Compound055")
shConRodAssyR = assyDoc.getObject("Compound049")
shPistonAssyR = assyDoc.getObject("Compound051")
shEccentricsR = assyDoc.getObject("Cut001005012034004002003001002003003001027")
shEccRodAssyRA = assyDoc.getObject("Compound043")
shEccRodAssyRB = assyDoc.getObject("Compound044")
shEccsLinkR = assyDoc.getObject("Cut001005012034004002003001002003003001025")
shEccsLinkStrapAssyR = assyDoc.getObject("Compound016")
shEccsLinkSlipperR = assyDoc.getObject("Cut001005012034004002003001002003003001026")
shEccsValveAssyR = assyDoc.getObject("Compound045")
shConRodAssyL = assyDoc.getObject("Compound050")
shPistonAssyL = assyDoc.getObject("Compound052")
shEccentricsL = assyDoc.getObject("Cut001005012034004002003001002003003001047")
shEccRodAssyLA = assyDoc.getObject("Compound046")
shEccRodAssyLB = assyDoc.getObject("Compound047")
shEccsLinkL = assyDoc.getObject("Cut001005012034004002003001002003003001048")
shEccsLinkStrapAssyL = assyDoc.getObject("Compound020")
shEccsLinkSlipperL = assyDoc.getObject("Cut001005012034004002003001002003003001049")
shEccsValveAssyL = assyDoc.getObject("Compound048")
assyDoc = App.ActiveDocument
skDoc = App.ActiveDocument
# Define constants
fac = pi / 180 # For converting between radians and degrees
# Define geometry constants
CrankThrow = 13 # Distance between aces of crank shaft and crank pin
ConRodLength = 44 # Distance between axes of crank pin and snall end pin
Z0 = 38 # Z coordinate of crank shaft
armAngleFDeg = -14 # Angle of EccsArm from vertical for running forewards
armAngleBDeg = 14 # Angle of EccsArm from vertical for running backwards
# Define simulation control constants
periodMilliseconds = 30 # Period between animation steps
stepsPerRev = 36 # Number of steps per full rotation. MUST BE A DIVISOR OF 360!!
stepSizeDeg = 360 / stepsPerRev
SetupDualEngine()
# Create timer
timer = QtCore.QTimer()
theta = 0
# Ask operator for instructions
direction = '?'
timerIsConnected = False
askWhatToDo()
# Macro End: AnimateReversingEngines
- D: Changes the direction of the engine by animating the movement of the Reversing lever.
- R: Runs the engine until the user presses Enter
- Q: Terminates execution of the macro.
During the development of these models I encountered only one recurring problem. Under certain conditions which I have not managed to characterize, Compounds created with the Part WB lose their ability to retain the different colors assigned to their component shapes. It happens if a Compound is Copied and then Pasted. If you then delete the pasted Compound and create a new one from the same components the new one shows the correct colours again. But it can also happen spontaneously as can be seen in the overall assembly of the dual engine in which all Compounds representing the moving assemblies have lost their colors and are shown in default grey. I have deleted and replaced all these Compounds but the new ones show the true colors for a while and then revert to default grey.
While preparing this post I hit the problem that trying to save the 3D window when a Sketch is in editing mode doesn't work because the size of the text describing the constraints gets grossly enlarged.
ATTACHED FILES
A photo of the finished dual engine (56KB)
Screen shot of FreeCAD model (60KB)
Screen shot of the Sketch (24KB)
The Sketch file (8KB)
The Sketch Animation macro (1KB)
The Overall Assemblies file (3.6MB). This file is too large for uploading to the forum so I have made it available on Dropbox via the following link:
https://www.dropbox.com/s/qsjcuf6wkerf7 ... FCStd?dl=0
The Animation Macro (13KB)
FREECAD VERSION DATA
OS: Windows
Word size: 64-bit
Version: 0.14.3700 (Git)
Branch: releases/FreeCAD-0-14
Hash: 32f5aae0a64333ec8d5d160dbc46e690510c8fe1
Python version: 2.7.6
Qt version: 4.8.5
Coin version: 4.0.0a
SoQt version: 1.6.0a