FreeCad Scripting

FreeCad is a free (as in beer) CAD application.

There are far better CAD applications such as AutoCAD or SolidWorks but they are pretty much the exact opposite of free. If you’re looking to just CAD up a quick project then FreeCAD is probably your go to application. However it is (because it’s free) not the easiest application to get started with. I think probably there’s only really Blender that has a worse learning curve.

To help people like me (programmers with an Engineering background) get started with FreeCAD I’m quickly documenting a project I had to make a garden sofa out of decking boards.

You can download the FreeCAD sofa design here.
Firstly here’s a screenshot of FreeCAD with the project open :

FreeCAD sofa project
FreeCAD Sofa Project

How to start with FreeCAD

Workbenches

The first concept you need to understand is ‘workbenches’.
FreeCAD has multiple ‘workbenches’ which are essentially ‘themes’ that change the types of buttons, icons and views which are easily accessible. You’ll need to start with the ‘Part Design’ workbench. On the toolbar there is a pulldown list of workbenches, select ‘Part Design’ from that pull down.

Object Hierarchy

Looking at the image above you can see in the object tree on the left that we have the main file ‘garden_sofa_32mm_boards’ and under that we have several ‘groups’: left_assembly, middle_assembly, right_assembly etc.
A group is just like a ‘folder’, it does not appear in your drawing it just allows you to keep similar objects together in one place.

Look at where the ‘right_assembly’ group is exploded out and you’ll see under each group we have several ‘body’ objects: right_bracket, right_middle, right_front etc.
A body is a little like a group, only can be manipulated (it’s x, y, z coordinates, or it’s visibility etc. can be changed, and will affect what you see on screen), but it is still really a conceptual object. That is, in and of itself it does not display anything on the screen, but you must have one.

Under each ‘body’ we have a ‘pad’ : plankxxx etc.
A pad is a conceptual object which says how high into the third dimension a 2D object is ‘extruded’. That is to say if we drew a circle and wanted it to be a cylinder, we’d use a ‘pad’ on the circle to extrude the circle into 3D. So what are we extruding?

In our case, right at the bottom of the hierarchy is a ‘sketch’ (see bottom of blog for an example). That is a 2D drawing of a shape, which we are extruding using the ‘pad’. A sketch in Freecad is a 2D shape that has ‘constraints’. That is to say, I can draw a shape and tell FreeCAD that two of the lines MUST BE parallel, or must be perpendicular etc. In this way we can define a shape that is only modifyable in ways we determine (so can be stretched in the x direction but not in the y etc.).

In our case we create a sketch of some boards which may or may not have miter cuts on the ends (angled ends). We tell FreeCAD that the boards MUST BE 120mm wide (for this we use a variable defined in the spreadsheet table called ‘d’, but this is out of scope of this tutorial). And that they optionally can have edges which are not 90 degrees etc.

When we’ve drawn this sketch we can then use the ‘pad’ to extrude the sketch until it looks like a decking board. We can modify the sketch (change the length, or angles of the ends) and the pad will just recalculate.

Each PAD must have a sketch, but we can just copy some sketches where appropriate.

Placement

Once we’ve made a few sketches, and extruded them into planks, we must determine where these planks go. For this we can use ‘placement’.
Click on the ‘body’ for the plank you want to move then pull down the edit menu and select ‘placement’. Select the checkbox named ‘apply incremental’ and then change the position of the plank in the x, y, or z planes. Or the angle of rotation.

A quick note on FreeCAD rotation

By default FreeCAD uses Quaternions to specify angles. But we don’t because we’re human, so when doing a ‘placement’ use the ‘axis’ pulldown to select x, y or z before trying to alter the rotation of a body.
You’ll need to do one axis at a time (that is alter the angle then click ‘apply’ before moving onto the next axis.

Using placement like this we can place our boards into the shape of a sofa. FreeCAD has all sorts of idiosyncrasies that will frustrate you, but what is written above is the basics. After a while you will tire of opening the placement menus and tampering with dimensions. This is when we discover the real power of FreeCAD which is its python based macro language.

FreeCAD Macro

Here is an example macro that does ‘things’ with the sofa:

import time
import FreeCADGui

d = FreeCAD.ActiveDocument.getObjectsByLabel('d')[0]
board_width = float(d.board_width)
board_thickness = float(d.board_thickness)
outdir = "C:/Users/richard/Downloads/"

def getDocumentName():
	return FreeCAD.ActiveDocument.Name

def getSketchFromPad(pad):
	if not pad: raise ValueError("must pass a pad")
	g = pad.Group
	for o in g:
		if str(type(o)) == "<class 'Sketcher.SketchObject'>":
			return o
	return None

def getConstraint(padName, constraintName):
	pad = getPadByName(padName)
	sketch = getSketchFromPad(pad)
	current = sketch.getDatum(constraintName)
	print(padName +"[" + constraintName + "] : " + str(current.Value))
	

def getBoardLength(padName):
	try: 
		getConstraint(padName, 'Top Edge Length')
		return
	except: pass
	try: 
		getConstraint(padName, 'Length')
		return
	except: pass
	

def setBoardLength(padName, length):
	if not padName or not length: raise ValueError("must pass sensible values")
	try:
		setConstraint(padName, 'Top Edge Length', length, "mm")
		return
	except: pass
	try:
		setConstraint(padName, 'Length', length, "mm")
		return
	except: pass

def setConstraint(padName, constraintName, value, units):
	if not padName or not constraintName or not value or not units: raise ValueError("must pass sensible values")
	pad = getPadByName(padName)
	sketch = getSketchFromPad(pad)
	sketch.setDatum(constraintName, App.Units.Quantity(str(value) + ' ' + units))

def getPadByName(name):
	return FreeCAD.ActiveDocument.getObjectsByLabel(name)[0]

def getAngles(padName):
	if not padName: raise ValueError("must supply a pad name")
	p = getPadByName(padName)
	if not p: raise ValueError("no such pad")	
	#lets not work out how to use a quaternion! Use Euler angles in degrees	
	e = p.Placement.Rotation.toEuler()
	X = e[2] #Roll
	Y = e[1] #Pitch
	Z = e[0] #Yaw
	print("X=" + str(X) + " Y=" + str(Y) + " Z=" + str(Z))

def setAngles(padName, x = None, y = None, z = None):
	if not x and not y and not z: return None
	if not padName: raise ValueError("must supply a pad name")
	p = getPadByName(padName)
	if not p: raise ValueError("no such pad")	
	#lets not work out how to use a quaternion! Use Euler angles in degrees	
	e = p.Placement.Rotation.toEuler()
	X = e[2] #Roll
	Y = e[1] #Pitch
	Z = e[0] #Yaw
	#print("X=" + str(X) + " Y=" + str(Y) + " Z=" + str(Z))
	if x: X = x
	if y: Y = y
	if z: Z = z
	rot = FreeCAD.Rotation(Z,Y,X)
	p.Placement.Rotation = rot

def setPosition(padName,x = None, y = None, z = None):
	if not x and not y and not z: return None
	if not padName: raise ValueError("must supply a pad name")
	p = getPadByName(padName)
	if not p: raise ValueError("no such pad")	
	if x: p.Placement.Base.x = x
	if y: p.Placement.Base.y = y
	if z: p.Placement.Base.z = z

def getPosition(padName):
	if not padName: raise ValueError("must supply a pad name")
	p = getPadByName(padName)
	if not p: raise ValueError("no such pad")
	print(p.Label + " : x=" + str(p.Placement.Base.x) + " y=" + str(p.Placement.Base.y) + " z=" + str(p.Placement.Base.z))

def show_all():
	objects = FreeCAD.ActiveDocument.Objects
	for object in objects:
		try:
			object.ViewObject.Visibility = True
		except: pass
	hide_planes()

def hide_planes():
	objects = FreeCAD.ActiveDocument.Objects
	for object in objects:
		if object.isDerivedFrom("App::Origin"):
			object.ViewObject.Visibility = False

def hide_all():
	objects = FreeCAD.ActiveDocument.Objects
	for object in objects:
		if ("App::DocumentObjectGroup" == object.TypeId):
			for obj in object.Group:
			    obj.ViewObject.Visibility = False

def export_drawing(padName, fileName):
	hide_all()
	pad = getPadByName(padName)
	pad.ViewObject.Visibility = True
	sketch = getSketchFromPad(pad)
	sketch.ViewObject.Visibility = True
	Gui.getDocument(getDocumentName()).setEdit(sketch)
	Gui.ActiveDocument.ActiveView.fitAll()
	Gui.ActiveDocument.ActiveView.saveImage(outdir + fileName, 1000, 1000, 'White')
	Gui.getDocument(getDocumentName()).resetEdit()
	show_all()

def export_drawings():
	show_all()
	export_drawing("left_bottom", "bottom_x3.jpg")
	export_drawing("left_backrest", "backrest_x3.jpg")
	export_drawing("left_front", "backrest_x2.jpg")
	export_drawing("middle_front", "backrest_x1.jpg")
	export_drawing("left_middle", "middle_x3.jpg")
	export_drawing("left_armrest", "armrest_x2.jpg")
	export_drawing("sp0", "seat_planks_x4.jpg")
	export_drawing("bp0", "backrest_planks_x4.jpg")
	export_drawing("left_bracket", "backrest_brace_x3.jpg")
	export_drawing("left_floor_brace", "left_floor_brace_x1.jpg")
	export_drawing("right_floor_brace", "right_floor_brace_x1.jpg")

def reset():
	tilt_angle = 5.0
	seat_length = 2000.0
	left_end_y = 0.0
	right_end_y = left_end_y + seat_length - board_thickness
	mid_end_y = left_end_y + ((right_end_y - left_end_y) / 2)

	#left assembly
	setPosition("left_bottom", y = left_end_y, x = 125.0, z = 0.0)
	setAngles("left_bottom", x = 90.0, y = 0.0, z = 0.0)
	setBoardLength("left_bottom", 855.0)

	setPosition("left_backrest", y = left_end_y + -1 * board_thickness, x = 456.0, z = 0.0)
	setAngles("left_backrest", x = -90.0, y = -75.0, z=-180.0)
	setBoardLength("left_backrest", 890.0)

	setPosition("left_middle", y = left_end_y, x = 294.83, z = 137.0)
	setAngles("left_middle", x = 90.0, y = (-1 * tilt_angle), z = 0.0)
	setBoardLength("left_middle", 718.0)

	setPosition("left_front", y = left_end_y + -1 * board_thickness, x = 980.00, z = 0.0)
	setAngles("left_front", x = 0.0, y = -90.0, z = 90.0)
	setBoardLength("left_front", 600.0)

	setPosition("left_armrest", y = left_end_y + -106, z = 600, x = 125.0)
	setAngles("left_armrest", x = 0.0, y = 0.0, z = 0.0)
	setBoardLength("left_armrest", 832.0)

	setPosition("left_bracket", y = left_end_y + -1 * board_thickness, x = 245.0, z = 0)
	setAngles("left_bracket", x = -135.0, y = -90.0, z = -135.0)
	setBoardLength("left_bracket", 635.0)

	#right assembly
	setPosition("right_bottom", y = right_end_y, x = 125.0, z = 0.0)
	setAngles("right_bottom", x = 90.0, y = 0.0, z = 0.0)
	setBoardLength("right_bottom", 855.0)

	setPosition("right_backrest", y = right_end_y + board_thickness, x = 456.0, z = 0.0)
	setAngles("right_backrest", x = -90.0, y = -75.0, z=-180.0)
	setBoardLength("right_backrest", 890.0)

	setPosition("right_middle", y = right_end_y, x = 294.83, z = 137.0)
	setAngles("right_middle", x = 90.0, y = (-1 * tilt_angle), z = 0.0)
	setBoardLength("right_middle", 718.0)

	setPosition("right_front", y = right_end_y + board_thickness, x = 980.00, z = 0.0)
	setAngles("right_front", x = 0.0, y = -90.0, z = 90.0)
	setBoardLength("right_front", 600.0)

	setPosition("right_armrest", y = right_end_y - 44, z = 600, x = 125.0)
	setAngles("right_armrest", x = 0.0, y = 0.0, z = 0.0)
	setBoardLength("right_armrest", 830.0)

	setPosition("right_bracket", y = right_end_y + board_thickness, x = 245.0, z = 0)
	setAngles("right_bracket", x = -135.0, y = -90.0, z = -135.0)
	setBoardLength("right_bracket", 635.0)

	#mid assembly
	setPosition("middle_bottom", y = mid_end_y, x = 125.0, z = 0.0)
	setAngles("middle_bottom", x = 90.0, y = 0.0, z = 0.0)
	setBoardLength("middle_bottom", 855.0)

	setPosition("middle_backrest", y = mid_end_y + board_thickness, x = 456.0, z = 0.0)
	setAngles("middle_backrest", x = -90.0, y = -75.0, z=-180.0)
	setBoardLength("middle_backrest", 890.0)

	setPosition("middle_middle", y = mid_end_y, x = 294.83, z = 137.0)
	setAngles("middle_middle", x = 90.0, y = (-1 * tilt_angle), z = 0.0)
	setBoardLength("middle_middle", 718.0)

	setPosition("middle_front", y = mid_end_y + board_thickness, x = 980.00, z = 0.0)
	setAngles("middle_front", x = 0.0, y = -90.0, z = 90.0)
	setBoardLength("middle_front", 308.0)

	setPosition("middle_bracket", y = mid_end_y + board_thickness, x = 245.0, z = 0)
	setAngles("middle_bracket", x = -135.0, y = -90.0, z = -135.0)
	setBoardLength("middle_bracket", 635.0)

	#seat planks
	bx = 858.0
	spa = 150
	runner_length = seat_length
	setPosition("sp0", x = bx, y = -(board_thickness), z = 340.0)
	setBoardLength("sp0", runner_length)

	setPosition("sp1", x = bx - (spa * 1), y = -(board_thickness), z = 324.0)
	setBoardLength("sp1", runner_length)

	setPosition("sp2", x = bx - (spa * 2), y = -(board_thickness), z = 312.0)
	setBoardLength("sp2", runner_length)	
	setPosition("sp3", x = bx - (spa * 3), y = -(board_thickness), z = 298.0)
	setBoardLength("sp3", runner_length)

	runner_length = runner_length + (2 * board_thickness)

	#backrest planks
	setPosition("bp0", x = 367.0, y = -(board_thickness * 2), z = 330.0)
	setBoardLength("bp0",runner_length)
	setPosition("bp1", x = 328.0, y = -(board_thickness * 2), z = 474.0)
	setBoardLength("bp1",runner_length)
	setPosition("bp2", x = 286.0, y = -(board_thickness * 2), z = 630.0)
	setBoardLength("bp2",runner_length)
	setPosition("bp3", x = 248.0, y = -(board_thickness * 2), z = 775.0)
	setBoardLength("bp3",runner_length)

	#bracing boards
	setPosition("left_bracket", x = 245.0, y = -(board_thickness), z = 0.0)
	setAngles("left_bracket", x = 0.0, y = -90.0, z = 90.0)
	setPosition("middle_bracket", x = 245.0, y = mid_end_y + board_thickness, z = 0.0)
	setAngles("middle_bracket", x = 0.0, y = -90.0, z = 90.0)
	setPosition("right_bracket", x = 245.0, y = right_end_y + board_thickness , z = 0.0)
	setAngles("right_bracket", x = 0.0, y = -90.0, z = 90.0)

	bl = (seat_length / 2)
	setPosition("left_floor_brace", x = 700.0, y = 0.0, z = 0.0)
	setAngles("left_floor_brace", x=0.0, y=0.0, z=90)
	setBoardLength("left_floor_brace", bl)
	setPosition("right_floor_brace", x = 700.0, y = mid_end_y, z = 0.0)
	setAngles("right_floor_brace", x=0.0, y=0.0, z=90)
	setBoardLength("right_floor_brace", bl - board_thickness)

	FreeCAD.ActiveDocument.recompute()

reset()
export_drawings()

I’m not going to go into too much detail about how this script works but it does essentially 2 things :

An example of one of the sketches (as exported by the above script) is here :

FreeCAD sofa project
FreeCAD Sofa Project, armrest sketch

Here is a zip containing all the cutting list for the sofa so you can make one yourself. See the readme file in the zip.
Total length of decking required is something like (very roughly) 31M.
At the point of publishing 32mm x 120mm decking was around £5.00 per metre.
28mm x 120mm was more like £3.00/M
Then there’s the price of the cusions and paint etc. So you’d be looking at around £200.00 at the time of publishing.

I hope this helps.

There was a page here but I’ve replaced it with this GitHub project.