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.
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.
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.
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) 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 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 skCrankOtoA = sketch.Geometry skRodAtoC = sketch.Geometry skRodBtoD = sketch.Geometry skLinkLobeCtoE = sketch.Geometry skLinkLobeDtoF = sketch.Geometry skLinkEtoF = sketch.Geometry skLinkFtoG = sketch.Geometry skLinkStrapGtoH = sketch.Geometry skLinkArmHtoI =sketch.Geometry skValveRodKtoL = sketch.Geometry skControlLeverItoJ = sketch.Geometry # 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.
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
Word size: 64-bit
Version: 0.14.3700 (Git)
Python version: 2.7.6
Qt version: 4.8.5
Coin version: 4.0.0a
SoQt version: 1.6.0a