FreeCAD Control
FreeCAD (/Applications/FreeCAD.app) is scriptable in Python against the FreeCAD /
FreeCADGui / Part / Sketcher / PartDesign / Draft / Mesh / TechDraw modules.
Drive it through the FreeCAD Bridge — a small script running inside a running FreeCAD
GUI that executes whatever build script is POSTed to 127.0.0.1:8735. Because the script
runs in the live session on the GUI thread, the viewport updates as you build and
Gui.* / saveImage work directly. Everything below was proven on FreeCAD 1.1.1.
- scripts/freecad-bridge.py — the bridge to run inside FreeCAD.
- scripts/freecad-send.sh — send a
.py file (or -c 'inline code')
to the bridge and print its captured output; --ping checks it's up.
- scripts/example-bracket.py — a complete build→measure→export→
screenshot script to copy from.
- references/api-reference.md — condensed, version-checked
Python API (App/document, Part, Sketcher, PartDesign, Draft, Mesh, Import/Export, GUI).
Read it before writing anything past the cheatsheet below.
The control loop
User starts the bridge (one-time per session): FreeCAD must be running with
freecad-bridge.py executed inside it. This cannot be done remotely — if
bash scripts/freecad-send.sh --ping gets no answer, ask the user to open FreeCAD and run
the bridge, one of:
- paste the contents of scripts/freecad-bridge.py into the
Python console (View ▸ Panels ▸ Python console) and press Enter, or
- copy it into the FreeCAD Macro folder (
~/Library/Application Support/FreeCAD/Macro/,
verify via Macro ▸ Macros… ▸ User macros location) and run Macro ▸ Macros ▸
freecad-bridge ▸ Execute.
A successful ping returns {"ok": true, "bridge": "freecad", "version": "1.1.1"}.
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 error it
returns the traceback and the sender exits non-zero. FREECAD_SEND_TIMEOUT=600
(seconds) for heavy builds. App / FreeCAD / Gui are pre-imported in the script's
namespace, and OUT is injected (from $OUT) as both a global and os.environ["OUT"].
Feedback: the returned stdout is your first signal. For structured data write a
metrics.json (bounding box, volume, isValid(), solid count) and Read it; for a picture,
call Gui.activeDocument().activeView().saveImage(path, w, h, "White") in the same script
and Read the PNG. Export STEP/STL/FCStd into $OUT for the deliverable.
Iterate: inspect output/PNG, fix the script, re-send. Scripts must be re-runnable —
create a fresh App.newDocument("thing_v2") per version so re-sends don't collide with a
document already open in the session.
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.
Modeling cheatsheet (proven patterns)
Units are mm and degrees. App = FreeCAD (both pre-imported by the bridge). Always
recompute() before measuring.
Solid via primitives + boolean (verified: plate with a bored hole)
import os, json, Part, Mesh
OUT = globals().get("OUT") or os.environ["OUT"]
doc = App.newDocument("bracket_v1") # fresh, versioned
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
Measure + screenshot (the live feedback)
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
Gui.activeDocument().activeView().viewIsometric() # live GUI — snapshot inline
Gui.SendMsgToActiveView("ViewFit"); Gui.updateGui()
Gui.activeDocument().activeView().saveImage(OUT+"/check.png", 900, 675, "White")
Export (pick the module by format — see the reference's matrix)
Part.export([cut], OUT+"/bracket.step") # B-rep: .step/.stp .iges/.igs .brep
Mesh.export([cut], OUT+"/bracket.stl") # mesh: .stl .obj .ply
doc.saveAs(OUT+"/bracket.FCStd") # native, re-editable
Parametric route (Sketcher → PartDesign, verified: rectangle → pad)
import Part, Sketcher
body = doc.addObject("PartDesign::Body", "Body")
sk = body.newObject("Sketcher::SketchObject", "Sketch")
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()
Feature types, constraint kinds, Draft/Mesh/TechDraw calls, and the full export matrix are in
references/api-reference.md.
Verification
- Returned stdout is the immediate signal —
print(...) in the script comes straight back
through the sender.
- metrics.json (bbox / volume /
isValid() / 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,
saveImage(...) in the same
script produces a viewport PNG to Read — no separate render step.
- Hand-off: an exported
.stl/.step 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: an open document,
imported assets, and view settings all stick. Version your docs (
newDocument("x_v2")) so a
re-send doesn't stack objects onto a document already open.
Gui is live here — saveImage, viewIsometric, ViewFit all work. (This is the whole
reason for the bridge over headless -c, where there is no viewport.)
print() and captured stdout come back in the response; App.Console.PrintMessage goes
to FreeCAD's Report view, not to the sender — use print() for anything you want returned.
recompute() before every measure or export — parametric attributes don't reach
.Shape until then.
- Placement is value-typed — assign a fresh
App.Placement(...) (or reassign after
mutating) and recompute; in-place edits can silently no-op.
- 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 (hidden) —
cut.Base/cut.Tool still exist;
export only the result feature.
- Save the native
.FCStd from the script so parametric work survives and can be re-edited.
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
127.0.0.1:8735 (FREECAD_BRIDGE_PORT) and executes any Python POSTed
to /run 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
Origin header or a cross-site
Sec-Fetch-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 user runs the bridge. Both install routes (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.
1---2name: freecad3description: Remote-control a running FreeCAD (the open-source parametric CAD app) by Python script through a small local bridge — build and edit 3D models live: box/cylinder primitives and booleans, filleted/chamfered solids, constrained Sketcher profiles, parametric PartDesign features (pad, pocket, revolution, patterns), 2D Draft geometry, meshes, and TechDraw drawings, then measure them, grab viewport screenshots, and export STEP / IGES / BREP / STL / OBJ. Use whenever the user wants to create or edit a CAD model, 3D part, mechanical component, enclosure, bracket, or .FCStd file, convert or export a model to STEP/STL/IGES/OBJ, or says "FreeCAD", "CAD model", "parametric part", "make a 3D part", "export to STEP", "STL for printing" — even if they don't mention scripting. Also covers Hungarian: "csinálj egy CAD modellt", "3D alkatrész", "parametrikus alkatrész", "exportáld STEP-be", "STL nyomtatáshoz".4---56# FreeCAD Control78FreeCAD (`/Applications/FreeCAD.app`) is scriptable in **Python** against the `FreeCAD` /9`FreeCADGui` / `Part` / `Sketcher` / `PartDesign` / `Draft` / `Mesh` / `TechDraw` modules.10Drive it through the **FreeCAD Bridge** — a small script running inside a *running* FreeCAD11GUI that executes whatever build script is POSTed to `127.0.0.1:8735`. Because the script12runs in the live session on the GUI thread, the viewport updates as you build and13`Gui.*` / `saveImage` work directly. Everything below was proven on FreeCAD 1.1.1.1415- [scripts/freecad-bridge.py](scripts/freecad-bridge.py) — the bridge to run inside FreeCAD.16- [scripts/freecad-send.sh](scripts/freecad-send.sh) — send a `.py` file (or `-c 'inline code'`)17 to the bridge and print its captured output; `--ping` checks it's up.18- [scripts/example-bracket.py](scripts/example-bracket.py) — a complete build→measure→export→19 screenshot script to copy from.20- [references/api-reference.md](references/api-reference.md) — condensed, version-checked21 Python API (App/document, Part, Sketcher, PartDesign, Draft, Mesh, Import/Export, GUI).22 **Read it before writing anything past the cheatsheet below.**2324## The control loop25261. **User starts the bridge** (one-time per session): FreeCAD must be running with27 `freecad-bridge.py` executed inside it. This cannot be done remotely — if28 `bash scripts/freecad-send.sh --ping` gets no answer, ask the user to open FreeCAD and run29 the bridge, one of:30 - paste the contents of [scripts/freecad-bridge.py](scripts/freecad-bridge.py) into the31 **Python console** (View ▸ Panels ▸ Python console) and press Enter, or32 - copy it into the FreeCAD **Macro** folder (`~/Library/Application Support/FreeCAD/Macro/`,33 verify via Macro ▸ Macros… ▸ *User macros location*) and run **Macro ▸ Macros ▸34 freecad-bridge ▸ Execute**.3536 A successful ping returns `{"ok": true, "bridge": "freecad", "version": "1.1.1"}`.372. **Write a build script** to the scratchpad — Python against the API (start from38 [references/api-reference.md](references/api-reference.md) and39 [scripts/example-bracket.py](scripts/example-bracket.py)).403. **Send it**: `OUT=/path/to/outdir bash scripts/freecad-send.sh /path/build.py`. The bridge41 runs it in the live session and returns the script's **captured stdout**; on error it42 returns the **traceback** and the sender exits non-zero. `FREECAD_SEND_TIMEOUT=600`43 (seconds) for heavy builds. `App` / `FreeCAD` / `Gui` are pre-imported in the script's44 namespace, and `OUT` is injected (from `$OUT`) as both a global and `os.environ["OUT"]`.454. **Feedback**: the returned stdout is your first signal. For structured data write a46 `metrics.json` (bounding box, volume, `isValid()`, solid count) and Read it; for a picture,47 call `Gui.activeDocument().activeView().saveImage(path, w, h, "White")` in the same script48 and Read the PNG. Export STEP/STL/FCStd into `$OUT` for the deliverable.495. **Iterate**: inspect output/PNG, fix the script, re-send. Scripts must be **re-runnable** —50 create a fresh `App.newDocument("thing_v2")` per version so re-sends don't collide with a51 document already open in the session.5253A failed send prints the Python traceback the bridge captured. The bridge stays up across54sends; if it becomes unresponsive, ask the user to re-run the bridge script.5556## Modeling cheatsheet (proven patterns)5758Units are **mm** and **degrees**. `App` = `FreeCAD` (both pre-imported by the bridge). Always59`recompute()` before measuring.6061### Solid via primitives + boolean (verified: plate with a bored hole)6263```python64import os, json, Part, Mesh65OUT = globals().get("OUT") or os.environ["OUT"]66doc = App.newDocument("bracket_v1") # fresh, versioned67box = doc.addObject("Part::Box", "Plate"); box.Length, box.Width, box.Height = 40, 20, 1068hole = doc.addObject("Part::Cylinder", "Hole"); hole.Radius, hole.Height = 5, 1069hole.Placement.Base = App.Vector(20, 10, 0) # centre of the plate70cut = doc.addObject("Part::Cut", "Bracket"); cut.Base, cut.Tool = box, hole71doc.recompute() # REQUIRED before measuring72```7374### Measure + screenshot (the live feedback)7576```python77shp = cut.Shape; bb = shp.BoundBox78m = {"bbox": [round(bb.XLength,3), round(bb.YLength,3), round(bb.ZLength,3)],79 "volume": round(shp.Volume,3), "valid": shp.isValid(), "solids": len(shp.Solids)}80json.dump(m, open(OUT+"/metrics.json","w"), indent=2)81print("METRICS", json.dumps(m)) # returned to the sender8283Gui.activeDocument().activeView().viewIsometric() # live GUI — snapshot inline84Gui.SendMsgToActiveView("ViewFit"); Gui.updateGui()85Gui.activeDocument().activeView().saveImage(OUT+"/check.png", 900, 675, "White")86```8788### Export (pick the module by format — see the reference's matrix)8990```python91Part.export([cut], OUT+"/bracket.step") # B-rep: .step/.stp .iges/.igs .brep92Mesh.export([cut], OUT+"/bracket.stl") # mesh: .stl .obj .ply93doc.saveAs(OUT+"/bracket.FCStd") # native, re-editable94```9596### Parametric route (Sketcher → PartDesign, verified: rectangle → pad)9798```python99import Part, Sketcher100body = doc.addObject("PartDesign::Body", "Body")101sk = body.newObject("Sketcher::SketchObject", "Sketch")102V = App.Vector103for a, b in [((0,0),(30,0)), ((30,0),(30,20)), ((30,20),(0,20)), ((0,20),(0,0))]:104 sk.addGeometry(Part.LineSegment(V(*a,0), V(*b,0)), False)105for i in range(4):106 sk.addConstraint(Sketcher.Constraint("Coincident", i, 2, (i+1) % 4, 1))107doc.recompute()108pad = body.newObject("PartDesign::Pad", "Pad"); pad.Profile = sk; pad.Length = 10109doc.recompute()110```111112Feature types, constraint kinds, Draft/Mesh/TechDraw calls, and the full export matrix are in113[references/api-reference.md](references/api-reference.md).114115## Verification116117- **Returned stdout** is the immediate signal — `print(...)` in the script comes straight back118 through the sender.119- **metrics.json** (bbox / volume / `isValid()` / solid count) is the structured check — write120 it and Read it to confirm geometry without eyeballing.121- **Inline screenshot**: because the bridge runs in the live GUI, `saveImage(...)` in the same122 script produces a viewport PNG to Read — no separate render step.123- **Hand-off**: an exported `.stl`/`.step` opens in any slicer/CAD tool the user already has.124125## Gotchas (hard-won on this machine)126127- **The bridge runs in the live session**, so state persists between sends: an open document,128 imported assets, and view settings all stick. Version your docs (`newDocument("x_v2")`) so a129 re-send doesn't stack objects onto a document already open.130- **`Gui` is live here** — `saveImage`, `viewIsometric`, `ViewFit` all work. (This is the whole131 reason for the bridge over headless `-c`, where there is no viewport.)132- **`print()` and captured stdout** come back in the response; `App.Console.PrintMessage` goes133 to FreeCAD's Report view, *not* to the sender — use `print()` for anything you want returned.134- **`recompute()` before every measure or export** — parametric attributes don't reach135 `.Shape` until then.136- **Placement is value-typed** — assign a fresh `App.Placement(...)` (or reassign after137 mutating) and recompute; in-place edits can silently no-op.138- **PySide binding**: FreeCAD 1.1 ships **PySide6** (Qt6); the bridge imports it with a139 PySide2 fallback. If you script Qt directly, import the same way.140- **Booleans keep their inputs in the tree** (hidden) — `cut.Base`/`cut.Tool` still exist;141 export only the result feature.142- Save the native `.FCStd` from the script so parametric work survives and can be re-edited.143144## Security145146Running the bridge means running a **code-execution server** on the user's machine. Say so147before asking them to start it.148149- The bridge binds `127.0.0.1:8735` (`FREECAD_BRIDGE_PORT`) and executes any Python POSTed150 to `/run` inside the live session — the user's privileges, the user's open documents.151 Requests carry **no authentication**: every local process, and every other user on a152 shared machine, can drive FreeCAD through it.153- Web pages **cannot**. Requests carrying an `Origin` header or a cross-site154 `Sec-Fetch-Site` are rejected with 403, so a page in the user's browser can't reach the155 bridge. That check is the only gate — there is no token.156- **Nothing listens until the user runs the bridge.** Both install routes (Python console157 paste, Macro ▸ Execute) are per-session — the Macro folder copy only saves the pasting,158 it does not autostart. Turning it into an autoload macro would make FreeCAD listen on159 every launch; don't suggest that without saying so.160- **To stop the bridge, quit FreeCAD.** There is no remote shutdown; the port is released161 with the process.