FreeCAD Control
FreeCAD (
/Applications/FreeCAD.app
) is scriptable in
Python against the
/
/
/
/
/
/
/
modules.
Drive it through the
FreeCAD Bridge — a small script running inside a
running FreeCAD
GUI that executes whatever build script is POSTed to
. Because the script
runs in the live session on the GUI thread, the viewport updates as you build and
/
work directly. Everything below was proven on FreeCAD 1.1.3.
- scripts/freecad-bridge.py — the bridge to run inside FreeCAD.
- scripts/freecad-send.sh — send a file (or )
to the bridge and print its captured output; checks it's up.
- scripts/example-bracket.py — a complete build→check→measure→
export→screenshot script to copy from.
- references/api-reference.md — condensed, version-checked
Python API (document, Part, Sketcher attachment, PartDesign features, Draft, Mesh,
TechDraw drawings, import/export, GUI). Read it before writing anything past the
cheatsheet below — several PartDesign/TechDraw properties are not what you'd guess.
The control loop
- Make sure the bridge is up:
bash scripts/freecad-send.sh --ping
. A live bridge
answers {"ok": true, "bridge": "freecad", "version": "1.1.3"}
. If it doesn't, the bridge
has to be started — it is a local code-execution server (see Security), so
say that before starting it:
- FreeCAD not running ( finds nothing): launch it with the bridge
as a startup script — FreeCAD executes a passed on its command line:
open -a FreeCAD --args "$PWD/scripts/freecad-bridge.py"
(absolute path), then poll
for up to ~60 s while the GUI starts.
- FreeCAD already running: doesn't hand arguments to a running app, so ask the
user to paste scripts/freecad-bridge.py into the Python
console (View ▸ Panels ▸ Python console) or run it via Macro ▸ Macros… ▸ Execute
after copying it into the user macro folder (shown in that dialog).
- Write a build script to the scratchpad — Python against the API (start from
references/api-reference.md and
scripts/example-bracket.py).
- Send it:
OUT=/path/to/outdir bash scripts/freecad-send.sh /path/build.py
. The bridge
runs it in the live session and returns the script's captured stdout; on a Python
exception it returns the traceback and the sender exits non-zero.
(seconds) for heavy builds. / / are
pre-imported in the script's namespace, and is injected (from ) as both a
global and .
- Check, then measure: a feature that fails to compute does not raise — see the
recompute check below. Then write a
(bounding box, volume, , solid count) and Read it; for a
picture, in the same script and Read the PNG. Export STEP/STL/FCStd into
for the deliverable.
- Iterate: inspect output/PNG, fix the script, re-send. Scripts must be re-runnable:
close any document of the same name before (FreeCAD silently
renames a duplicate to , and the old one keeps cluttering the session). Don't
close documents you didn't create — the session is the user's.
A failed send prints the Python traceback the bridge captured. The bridge stays up across
sends; if it becomes unresponsive, ask the user to re-run the bridge script.
No GUI needed? For pure build→measure→export work (no screenshot, no TechDraw PDF),
/Applications/FreeCAD.app/Contents/Resources/bin/freecadcmd build.py
runs the same script
headless — useful when the user can't start the bridge. There
is undefined (guard with
), stdout carries FreeCAD's own log noise, and an
uncaught exception
still exits 0 — wrap the script in
and
so failures surface.
Modeling cheatsheet (proven patterns)
Units are
mm and
degrees.
=
(both pre-imported by the bridge). Always
before measuring.
Solid via primitives + boolean (verified: plate with a bored hole)
python
import os, json, Part, Mesh
OUT = globals().get("OUT") or os.environ["OUT"]
if "bracket" in App.listDocuments(): # re-runnable: drop the previous build
App.closeDocument("bracket")
doc = App.newDocument("bracket")
box = doc.addObject("Part::Box", "Plate"); box.Length, box.Width, box.Height = 40, 20, 10
hole = doc.addObject("Part::Cylinder", "Hole"); hole.Radius, hole.Height = 5, 10
hole.Placement.Base = App.Vector(20, 10, 0) # centre of the plate
cut = doc.addObject("Part::Cut", "Bracket"); cut.Base, cut.Tool = box, hole
doc.recompute() # REQUIRED before measuring
Check, measure, screenshot (the live feedback)
never raises: a broken feature (bad sketch, pocket that misses the solid,
fillet too big) is marked
and its
is stale or empty, so the metrics would
quietly describe the wrong part. Check first:
python
bad = [(o.Name, o.State) for o in doc.Objects if "Invalid" in o.State or "Error" in o.State]
if bad:
raise RuntimeError("recompute failed: %s" % bad) # comes back as the send's traceback
shp = cut.Shape; bb = shp.BoundBox
m = {"bbox": [round(bb.XLength,3), round(bb.YLength,3), round(bb.ZLength,3)],
"volume": round(shp.Volume,3), "valid": shp.isValid(), "solids": len(shp.Solids)}
json.dump(m, open(OUT+"/metrics.json","w"), indent=2)
print("METRICS", json.dumps(m)) # returned to the sender
view = Gui.getDocument(doc.Name).activeView() # live GUI — snapshot inline
view.setAnimationEnabled(False) # else saveImage catches the camera mid-turn
view.viewIsometric(); view.fitAll()
view.saveImage(OUT+"/check.png", 900, 675, "White")
Compare the metrics against what the user asked for (e.g. expected volume computed by hand)
— that catches a hole in the wrong place or a pad in the wrong direction, which a valid,
single-solid shape would not.
Export (pick the module by format — see the reference's matrix)
python
Part.export([cut], OUT+"/bracket.step") # B-rep: .step/.stp .iges/.igs .brep
Mesh.export([cut], OUT+"/bracket.stl") # mesh: .stl .obj .ply .3mf .amf .off
doc.saveAs(OUT+"/bracket.FCStd") # native, re-editable
Parametric route (Sketcher → PartDesign, verified: rectangle → pad)
python
import Part, Sketcher
body = doc.addObject("PartDesign::Body", "Body")
sk = body.newObject("Sketcher::SketchObject", "Sketch") # unattached = lies on XY
V = App.Vector
for a, b in [((0,0),(30,0)), ((30,0),(30,20)), ((30,20),(0,20)), ((0,20),(0,0))]:
sk.addGeometry(Part.LineSegment(V(*a,0), V(*b,0)), False)
for i in range(4):
sk.addConstraint(Sketcher.Constraint("Coincident", i, 2, (i+1) % 4, 1))
doc.recompute()
pad = body.newObject("PartDesign::Pad", "Pad"); pad.Profile = sk; pad.Length = 10
doc.recompute()
When the user wants to edit dimensions later, drive them with dimensional constraints
(
,
,
, …) and confirm
is
. To
sketch on another plane or on a face, set
+
;
through-all pockets are
pocket.Type = "ThroughAll"
; after adding a Linear/PolarPattern set
yourself. All are in the reference, with revolution axes, fillets, and how
to pick edges by geometry instead of guessing
.
Feature types, constraint kinds, Draft/Mesh calls, TechDraw drawings, and the full export
matrix are in references/api-reference.md.
Verification
- Returned stdout is the immediate signal — in the script comes straight back
through the sender.
- The -state check is what turns a silent recompute failure into a traceback.
- metrics.json (bbox / volume / / solid count) is the structured check — write
it and Read it to confirm geometry without eyeballing.
- Inline screenshot: because the bridge runs in the live GUI, in the same
script produces a viewport PNG to Read — no separate render step. For a TechDraw PDF,
qlmanage -t -s 1000 -o <dir> page.pdf
renders a PNG thumbnail to Read.
- Hand-off: an exported / opens in any slicer/CAD tool the user already has.
Gotchas (hard-won on this machine)
- The bridge runs in the live session, so state persists between sends: open documents,
imported assets, and view settings all stick. Close-then-create your own document by name
so a re-send starts clean.
- never fails on a duplicate name — it silently creates , so
later returns the old build.
- Failed features don't raise — returns normally and leaves the object
. Check before trusting any measurement.
- is live here — , , all work. (Under headless
it is not defined.)
- Blank or off-angle screenshots come from view animation: starts a camera
animation that doesn't wait for.
view.setAnimationEnabled(False)
first. Always
Read the PNG before handing it over.
- and captured stdout come back in the response; goes
to FreeCAD's Report view, not to the sender — use for anything you want returned.
- before every measure or export — parametric attributes don't reach
until then.
- TechDraw views compute in a background thread — right after a view has no
edges yet, and a dimension referencing measures . Wait for
before adding dimensions (pattern in the reference). A view's
is ignored unless , and edge lengths come back scaled.
- TechDraw exports skip the frame until the page has been opened in the GUI
(
page.ViewObject.doubleClicked()
), and the default A4 template is an empty sheet — use the
ISO templates (reference).
- A pattern doesn't become the Body's Tip — keeps showing the unpatterned
solid until you set .
- Guessed edge/face numbers break — is often a seam; select by curve type,
radius or position (snippet in the reference).
- PySide binding: FreeCAD 1.1 ships PySide6 (Qt6); the bridge imports it with a
PySide2 fallback. If you script Qt directly, import the same way.
- Booleans keep their inputs in the tree — / hide /
automatically, but / leave their base visible (it shows through in
screenshots). Hide inputs yourself; export only the result feature.
- STL resolution: in the GUI, reuses the coarse display mesh. For anything
printed, mesh explicitly with
MeshPart.meshFromShape(...)
(reference) and report the facet
count.
- Save the native from the script so parametric work survives and can be re-edited
Re-saving over an existing file leaves a timestamped beside it — delete it from
if the folder is a deliverable.
Security
Running the bridge means running a code-execution server on the user's machine. Say so
before asking them to start it.
- The bridge binds () and executes any Python POSTed
to inside the live session — the user's privileges, the user's open documents.
Requests carry no authentication: every local process, and every other user on a
shared machine, can drive FreeCAD through it.
- Web pages cannot. Requests carrying an header or a cross-site
are rejected with 403, so a page in the user's browser can't reach the
bridge. That check is the only gate — there is no token.
- Nothing listens until the bridge is started. All three routes (launch with the script
as an argument, Python console paste, Macro ▸ Execute) are per-session — the Macro folder
copy only saves the pasting, it does not autostart. Turning it into an autoload macro would make FreeCAD listen on
every launch; don't suggest that without saying so.
- To stop the bridge, quit FreeCAD. There is no remote shutdown; the port is released
with the process.