Refactoring FreeCAD's Python Output
Why Bother?
Every click you make in FreeCAD — adding a sketch, extruding a pad, applying a fillet — is silently translated into Python commands and echoed in the Report View / Python console. This is FreeCAD's live macro recorder, and most users walk straight past it.
The output is not pretty. It is verbose, full of hardcoded magic numbers, and riddled with GUI-interaction calls that have no place in an automated script. But buried inside it is the complete recipe for your model. Refactoring that recipe into a proper script means you can:
- Regenerate identical geometry programmatically with zero GUI interaction.
- Parameterize dimensions so one variable change rebuilds the whole model.
- Version-control your design like any other piece of source code.
- Drive batch generation — produce fifty size variants in a loop.
Step 1 — Capture the Console Output
Open FreeCAD, build a simple solid through the GUI, then copy everything from the Report View (View → Panels → Report View) or directly from the embedded Python console. You will see something like this:
# --- raw output from FreeCAD console --- import FreeCAD, Part, Sketcher import FreeCADGui Gui.activateWorkbench("PartDesignWorkbench") App.newDocument("Unnamed") App.setActiveDocument("Unnamed") App.ActiveDocument.addObject('PartDesign::Body', 'Body') Gui.activeView().setActiveObject('pdbody', App.ActiveDocument.Body) App.ActiveDocument.Body.newObject('Sketcher::SketchObject', 'Sketch') App.ActiveDocument.Sketch.Support = (App.ActiveDocument.XY_Plane, ['']) App.ActiveDocument.Sketch.MapMode = 'FlatFace' App.ActiveDocument.Sketch.addGeometry(Part.LineSegment( App.Vector(0, 0, 0), App.Vector(40, 0, 0)), False) App.ActiveDocument.Sketch.addGeometry(Part.LineSegment( App.Vector(40, 0, 0), App.Vector(40, 20, 0)), False) App.ActiveDocument.Sketch.addGeometry(Part.LineSegment( App.Vector(40, 20, 0), App.Vector(0, 20, 0)), False) App.ActiveDocument.Sketch.addGeometry(Part.LineSegment( App.Vector(0, 20, 0), App.Vector(0, 0, 0)), False) Gui.ActiveDocument.Sketch.Visibility = False App.ActiveDocument.Body.newObject('PartDesign::Pad', 'Pad') App.ActiveDocument.Pad.Profile = App.ActiveDocument.Sketch App.ActiveDocument.Pad.Length = 10 App.ActiveDocument.recompute() Gui.activeDocument().resetEdit()
.FCMacro file and avoids manually scraping the Report View.
Step 2 — Identify the Noise
Before refactoring, understand what you can safely remove:
FreeCADGui/Gui.*callsactivateWorkbench().VisibilityassignmentsresetEdit()setActiveObject()
addObject()callsaddGeometry()calls- Constraint additions
- Feature properties (Length, etc.)
recompute()
FreeCADCmd or in CI) will crash on any FreeCADGui import if a display is not available. Guard every GUI call with if FreeCAD.GuiUp:.
Step 3 — Parameterize the Magic Numbers
The single most valuable transformation is hoisting the raw numbers into named variables at the top of the file. In the raw console output above, 40, 20, and 10 are the width, height, and extrusion depth of a simple block — but you would never know that without reading the entire file.
A good rule of thumb: if a number appears in a geometric context (a coordinate, a length, an angle), it belongs in the parameters block. If it is a constant tied to FreeCAD internals (e.g., constraint type enumerations), leave it inline.
Step 4 — Wrap in a Function
Wrap the body logic in a function that accepts parameters. This enables you to call it from another script, loop over it, or import it as a module. Use a doc parameter so the caller can supply an existing document, or let the function create its own.
Complete Refactored Script
""" make_block.py Creates a simple rectangular pad in FreeCAD. Runs headlessly via: FreeCADCmd make_block.py or imported as a module from another script. """ import FreeCAD as App import Part import Sketcher # noqa: F401 – required for Sketcher objects # ── Parameters ────────────────────────────────────────────── WIDTH = 40.0 # mm, X dimension of the sketch rectangle HEIGHT = 20.0 # mm, Y dimension of the sketch rectangle DEPTH = 10.0 # mm, extrusion length DOC_NAME = "ParametricBlock" OUTPUT = "block.FCStd" # set to None to skip saving # ──────────────────────────────────────────────────────────── def make_block(width=WIDTH, height=HEIGHT, depth=DEPTH, doc_name=DOC_NAME, doc=None): """ Build a padded rectangular block and return the document. Parameters ---------- width : float – sketch width (X axis), mm height : float – sketch height (Y axis), mm depth : float – pad extrusion depth, mm doc_name: str – name for a new document (ignored if doc given) doc : App.Document or None – supply an existing document Returns ------- App.Document """ # ── 1. Document setup ──────────────────────────────────── if doc is None: doc = App.newDocument(doc_name) # ── 2. PartDesign Body ─────────────────────────────────── body = doc.addObject("PartDesign::Body", "Body") # ── 3. Sketch on XY plane ──────────────────────────────── sketch = body.newObject("Sketcher::SketchObject", "Sketch") sketch.Support = (doc.XY_Plane, [""]) sketch.MapMode = "FlatFace" # Build rectangle from corner coordinates corners = [ ( 0, 0), (width, 0), (width, height), ( 0, height), ] for i in range(4): p1 = App.Vector(corners[i][0], corners[i][1], 0) p2 = App.Vector(corners[(i + 1) % 4][0], corners[(i + 1) % 4][1], 0) sketch.addGeometry(Part.LineSegment(p1, p2), False) # ── 4. Pad feature ─────────────────────────────────────── pad = body.newObject("PartDesign::Pad", "Pad") pad.Profile = sketch pad.Length = depth # ── 5. Recompute ───────────────────────────────────────── doc.recompute() return doc def batch_sizes(size_list, output_dir="."): """Generate one .FCStd file per (width, height, depth) tuple.""" import os for w, h, d in size_list: name = f"block_{w}x{h}x{d}" doc = make_block(width=w, height=h, depth=d, doc_name=name) path = os.path.join(output_dir, f"{name}.FCStd") doc.saveAs(path) App.closeDocument(doc.Name) print(f"Saved: {path}") # ── Run when executed directly ─────────────────────────────── if __name__ == "__main__": result_doc = make_block() if OUTPUT: result_doc.saveAs(OUTPUT) print(f"Saved to {OUTPUT}") # Example batch run — uncomment to generate a size family: # batch_sizes([(40,20,10), (80,40,20), (120,60,30)])
What Changed, and Why
Parameters at the top
All magic numbers are lifted into named constants at module level. A downstream script or CI job can override them by importing the module and passing keyword arguments to make_block() — no text-substitution hacks needed.
Loop instead of repeated calls
The four addGeometry lines in the raw output were structurally identical — only the coordinates differed. A loop over a corners list makes the pattern explicit and eliminates the risk of a copy-paste mistake introducing a wrong coordinate. For more complex profiles, this generalizes to iterating over a list of points read from a CSV or computed by a formula.
Function + __main__ guard
Wrapping the logic in a function means another script can do from make_block import make_block and treat it as a library call. The if __name__ == "__main__" guard ensures the geometry is only built when the file is run directly, not on import.
No GUI imports
Every Gui.* call has been removed. The script runs identically in the interactive FreeCAD GUI (via Macro → Execute), in FreeCADCmd on a headless server, or inside a Python test runner. If you do need GUI interactions (e.g., to force a 3D view refresh during development), gate them behind if App.GuiUp: import FreeCADGui as Gui.
Spreadsheet workbench lets you bind feature properties to cell values — but for fully automated pipelines, a plain Python dict or CSV is simpler and more portable.
Running the Script
Inside the FreeCAD GUI
Open the Macro dialog (Macro → Macros…), point it at make_block.py, and click Execute. The model appears in the active document.
Headlessly from the terminal
# Linux / macOS (adjust path to your FreeCAD install) /usr/lib/freecad/bin/FreeCADCmd make_block.py # Windows "C:\Program Files\FreeCAD 0.21\bin\FreeCADCmd.exe" make_block.py
From another Python script
from make_block import make_block, batch_sizes # Single part doc = make_block(width=60, height=30, depth=15) doc.saveAs("custom_block.FCStd") # Whole size family sizes = [(w, w/2, w/4) for w in range(20, 120, 20)] batch_sizes(sizes, output_dir="./output")