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:

  1. Regenerate identical geometry programmatically with zero GUI interaction.
  2. Parameterize dimensions so one variable change rebuilds the whole model.
  3. Version-control your design like any other piece of source code.
  4. 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_console_output.py — unedited paste
# --- 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()
Tip: To capture output reliably, go to Macro → Macros… → Record before you start modelling. This writes directly to a .FCMacro file and avoids manually scraping the Report View.

Step 2 — Identify the Noise

Before refactoring, understand what you can safely remove:

Remove — GUI noise
Keep — geometry logic
  • FreeCADGui / Gui.* calls
  • activateWorkbench()
  • .Visibility assignments
  • resetEdit()
  • setActiveObject()
  • addObject() calls
  • addGeometry() calls
  • Constraint additions
  • Feature properties (Length, etc.)
  • recompute()
Warning: Scripts intended to run headlessly (e.g., via 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 — refactored & parameterized
"""
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.

Going further: Once your script is parameterized, you can drive it from a spreadsheet. FreeCAD's 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

shell
# 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

driver.py
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")