A comprehensive collection of Python utilities for converting FreeCAD designs to 3MF format optimized for 3D printing with PrusaSlicer. Automate exports, embed metadata, position bodies, and preserve printer settings.
freecad_tools bridges FreeCAD with modern 3D printing workflows by providing:
- 🎯 3MF Export: Convert FreeCAD bodies to 3MF files with embedded mesh data, ready for PrusaSlicer
- 🔄 Body Rotation & Positioning: Specify exact orientation and position in config (no manual adjustment in slicer needed)
- 📊 Metadata Embedding: Embed project info, version, author, and git metadata directly in 3MF files
- 📄 TechDraw Export: Extract technical drawings to PDF for documentation
- 📋 Bill of Materials: Auto-generate BOM CSV from assemblies with custom fields
- 🏷️ Body Marking: Mark bodies in FreeCAD for automatic export detection
- ⚙️ Parametric Variants: Generate multiple design variations using FreeCAD macros
- 📋 Template Support: Preserve PrusaSlicer printer settings across exports
- 🔗 Git Integration: Automatically capture commit hash, branch, and tags in exports
- 🚀 Automation: Use with pre-commit hooks for automatic exports on push
-
Copy the example config to your FreeCAD project:
mkdir -p MyProject/.freecad_tools cp /path/to/freecad_tools/examples/export_config.yml.example.yml MyProject/.freecad_tools/export.yml
-
Edit the config to specify your FreeCAD file and bodies:
export: - name: MyAntenna source: MyAntenna.FCStd bodies: - Feed001 - Cover001 output: prints/MyAntenna.3mf
-
Run the export:
cd MyProject python3 /path/to/freecad_tools/tools/export.py -
Open in PrusaSlicer:
open prints/MyAntenna.3mf
That's it! Your 3MF file is ready for printing.
# Auto-discover config in current directory
python3 tools/export.py
# Specify config file
python3 tools/export.py path/to/config.yml
python3 tools/export.py --config path/to/config.yml
# Validate config without exporting
python3 tools/export.py path/to/config.yml --dry-run
# Build slicer commands but do not execute slicers
python3 tools/export.py path/to/config.yml --slicer-dry-run
# Verbose logging
python3 tools/export.py path/to/config.yml --verbose
python3 tools/export.py path/to/config.yml -v
# List available export item names
python3 tools/export.py path/to/config.yml --list-exports
# Run only GUI tasks (screenshots + TechDraw), skip 3MF rebuild
python3 tools/export.py path/to/config.yml --gui-only
# Run only screenshots, skip 3MF and TechDraw
python3 tools/export.py path/to/config.yml --screenshots-only
# Queue GUI work and run one shared GUI session for the full run
python3 tools/export.py path/to/config.yml --gui-session run
# Redirect all generated outputs (prints/docs/images/bom/gcode)
python3 tools/export.py path/to/config.yml --output-root generated
# Show help
python3 tools/export.py --help| Option | Short | Description |
|---|---|---|
--config PATH |
-c PATH |
Specify YAML config file path |
--verbose |
-v |
Enable debug logging output |
--dry-run |
Validate config without performing export | |
--slicer-dry-run |
Build slicer commands but skip slicer execution | |
--name NAME |
-n NAME |
Export only the item with this name (from multi-item config) |
--list-exports |
Print available export item names and exit | |
--gui-only |
Run GUI tasks only (screenshots/TechDraw), skip 3MF export | |
--screenshots-only |
Run screenshot tasks only, skip 3MF and TechDraw | |
--gui-session MODE |
GUI batching mode: item (default) or run (shared GUI session for all queued GUI jobs) |
|
--output-root PATH |
Override base output directory for generated files | |
--help |
-h |
Show usage information |
GUI Session Modes:
item(default): run GUI tasks as each export item is processed.run: queue GUI tasks and execute them in one shared FreeCAD GUI session after non-GUI export steps.
For practical end-to-end runs in this repository:
make exportTo remove generated artifacts from test_output/:
make cleanEquivalent just recipes:
just export
just export-list
just export-item <name>
just gcode-bounds test_output/gcode/<file>.gcodeThis repository can build and publish a container image to GHCR via:
.github/workflows/build-container-image.yml
Triggers:
- Pushes to branches (including
main) - Version tags (
v*) - Manual run (
workflow_dispatch)
Published image tags include:
latestfor slim image (default branch)freecad-latestfor FreeCAD-enabled image (default branch)- variant-prefixed branch/tag/SHA tags (for example
slim-main,freecad-main,slim-<sha>,freecad-<sha>)
Example usage:
docker pull ghcr.io/ebirn/freecad_tools:latest
docker run --rm -v "$PWD:/workspace" ghcr.io/ebirn/freecad_tools:latest tests/export_test_config.yml --dry-run
docker pull ghcr.io/ebirn/freecad_tools:freecad-latest
docker run --rm -v "$PWD:/workspace" ghcr.io/ebirn/freecad_tools:freecad-latest tests/export_test_config.ymlUse :latest for lightweight tooling tasks and :freecad-latest for full export runs requiring FreeCAD.
Config File Discovery (when not specified):
.freecad_tools/export.yml(per-project config)export_config.yml(legacy config in current directory)
You can relocate generated outputs (3MF, TechDraw PDFs, screenshots, BOM CSV, slicer G-code) without changing individual paths in each export item.
Supported controls:
- CLI:
--output-root PATH - Environment:
FREECAD_TOOLS_OUTPUT_ROOT - Config: top-level
output_root: PATH
Precedence:
--output-rootFREECAD_TOOLS_OUTPUT_ROOToutput_rootin YAML config- Project root (default)
Example:
output_root: generated
export:
- name: Demo
source: example.FCStd
output: prints/demo.3mf
techdraw:
output_dir: docs
screenshots:
enabled: true
output_dir: docs/images
bom:
output: docs/bom.csvWith this config, files resolve under generated/:
generated/prints/demo.3mfgenerated/docs/Demo.pdfgenerated/docs/images/...generated/docs/bom.csv
NEW! Explicitly control how bodies are selected for export with the body_source field.
| Mode | Description | Use Case |
|---|---|---|
config |
Bodies explicitly listed in config | Full control, reproducible exports |
properties |
Bodies marked with ExportTo3MF property in FreeCAD |
Flexible, visual selection |
Bodies are specified explicitly in the YAML config. This is the default mode when bodies list is provided.
export:
- name: ExplicitExport
source: MyDesign.FCStd
body_source: config # Required when using explicit bodies list
bodies:
- Body1
- Body2
output: prints/output.3mfWhen to use:
- Reproducible exports (same bodies every time)
- CI/CD pipelines
- Multiple export configs for same document
- Version-controlled body selection
Bodies are selected automatically based on FreeCAD custom properties. No bodies list needed.
export:
- name: PropertyExport
source: MyDesign.FCStd
body_source: properties # No bodies list - use properties
output: prints/output.3mfRequired Properties (added automatically by macros/set_export_properties.py):
| Property | Type | Default | Description |
|---|---|---|---|
ExportTo3MF |
App::PropertyBool | false | Set to True to export this body |
ExportCount |
App::PropertyInteger | 1 | Number of copies to export |
ExportRotation |
App::PropertyRotation | identity | Orientation (axis+angle format) |
All properties are grouped under freecad_tools in the FreeCAD Properties panel.
Axis+Angle Rotation Format (matches FreeCAD GUI display):
rotation:
axis: [0, 0, 1] # X, Y, Z direction vector (normalized automatically)
angle: 45 # Degrees (positive = counter-clockwise)Example: Rotate 90° around X-axis:
rotation:
axis: [1, 0, 0]
angle: 90Why Axis+Angle?
- Unambiguous (no Euler angle convention confusion)
- No gimbal lock issues
- Matches what FreeCAD displays in Properties panel
FreeCAD.Rotationobjects use this format natively
If body_source is not specified, it is inferred from the bodies list:
- With
bodieslist: defaults toconfigmode (with deprecation warning) - Without
bodieslist: defaults topropertiesmode (with deprecation warning)
Migration: Add body_source: config or body_source: properties to your configs to suppress warnings.
Use the macros/set_export_properties.py macro to set export properties on bodies:
GUI Mode (run from FreeCAD Macro menu):
- Select bodies in the 3D view
- Run the macro
- All selected bodies get export properties with
ExportTo3MF=True
CLI Mode:
/Applications/FreeCAD.app/Contents/Resources/bin/freecadcmd \
/path/to/freecad_tools/macros/set_export_properties.py \
MyDesign.FCStd Body1 --count 3 --rotation-axis 0 0 1 --rotation-angle 45List all objects and their properties:
freecadcmd /path/to/freecad_tools/macros/set_export_properties.py MyDesign.FCStd --listExport bodies from FreeCAD to 3MF:
export:
- name: SimpleProject
source: MyDesign.FCStd
bodies:
- Body
- Body002
output: prints/SimpleProject.3mfWhat happens:
- Reads
MyDesign.FCStd - Exports "Body" and "Body002" as mesh objects
- Creates
prints/SimpleProject.3mf - Ready to open in PrusaSlicer
Position bodies exactly where they should be without manual adjustment.
Two rotation formats supported:
Euler Format (existing, backward compatible):
rotation: [0, 0, 45] # [X, Y, Z] degrees, intrinsic XYZ orderAxis+Angle Format (NEW - matches FreeCAD GUI):
rotation:
axis: [0, 0, 1] # Direction vector (normalized automatically)
angle: 45 # Degrees around that axisExample with both formats:
export:
- name: OrientedAntenna
source: Antenna.FCStd
bodies:
# Simple bodies (no rotation)
- Feed001
# Euler rotation: 45 degrees around Z-axis
- body: "Mounting Bracket"
rotation: [0, 0, 45]
# Axis+Angle rotation: 90 degrees around X-axis
- body: "Cable Guide"
rotation:
axis: [1, 0, 0]
angle: 90
position: [10, 0, 5] # X, Y, Z in mm
output: prints/Antenna_Positioned.3mfWhy this matters:
- ✅ Pre-positioned for optimal printing
- ✅ Save time in PrusaSlicer
- ✅ Consistent orientation across exports
- ✅ Supports multiple copies with different positions
- ✅ Axis+Angle format matches FreeCAD Properties panel display
Rotation Details:
- Euler:
rotation: [X, Y, Z]- degrees around each axis, intrinsic XYZ order - Axis+Angle:
rotation: {axis: [x, y, z], angle: deg}- direction vector + angle - Example Euler:
[45, 0, 0]= 45° around body's X-axis - Example Axis+Angle:
{axis: [0, 0, 1], angle: 90}= 90° around Z-axis
Position Details:
position: [X, Y, Z]- millimeter offset from origin- Example:
[10, 0, 0]= 10mm in positive X direction - Example:
[0, 20, 5]= 20mm in Y, 5mm in Z
Embed project information directly in the 3MF file:
export:
- name: MyProject
source: MyProject.FCStd
bodies:
- Body
output: prints/MyProject.3mf
metadata:
Project: "MyAntenna"
Version: "2.1"
Author: "Jane Smith"
Description: "Optimized for 0.4mm nozzle"Auto-Added Metadata (if in git repo):
GitCommit: Commit hash (short form)GitBranch: Current branch nameGitTags: Any tags on current commitGitRemote: Repository URL
View in PrusaSlicer:
- Open the 3MF file
- Metadata appears in object properties
- Track which version of the model is on your print bed
Export multiple designs from one config:
export:
- name: Antenna_Main
source: Antenna.FCStd
bodies:
- MainPart
- Feed
output: prints/Antenna_Main.3mf
- name: Antenna_Bracket
source: Antenna.FCStd
bodies:
- MountingBracket
output: prints/Antenna_Bracket.3mf
- name: CableGuide
source: CableGuide.FCStd
bodies:
- Guide
output: prints/CableGuide.3mfResult: Three 3MF files created, all with metadata and git info
Mark bodies in FreeCAD and let the tool find them automatically:
In FreeCAD:
- Select a body in the tree
- Add custom property:
- Name:
ExportTo3MF - Type: Boolean
- Value:
True
- Name:
- Repeat for all bodies to export
In config (leave bodies empty):
export:
- name: AutoExport
source: MyDesign.FCStd
bodies: [] # Empty = use marked bodies
output: prints/AutoExport.3mfBenefits:
- ✅ No need to update config when adding bodies
- ✅ Visual marking in FreeCAD (custom property is visible)
- ✅ Flexible - mark different sets for different exports
Sometimes you want to inspect the intermediate STL files:
export:
- name: DebugExport
source: MyDesign.FCStd
bodies:
- Body
output: prints/MyDesign.3mf
keep_stl: true
stl_output_dir: prints/stlResult:
prints/MyDesign.3mf- Final 3MF fileprints/stl/DebugExport_Body.stl- Intermediate mesh file
Use cases:
- Inspect mesh quality before printing
- Use in other tools (Meshmixer, etc.)
- Debug export issues
Extract technical drawings from your FreeCAD documents:
export:
- name: Antenna
source: Antenna.FCStd
bodies:
- MainBody
- Feed001
output: prints/Antenna.3mf
techdraw:
pages: [] # Empty = export all TechDraw pages
output_dir: docs # Where to save exported files
format: pdf # Currently only 'pdf' supported
instructions: INSTRUCTIONS.md # Optional: markdown rendered into PDFHow It Works: TechDraw PDF export uses a two-step pipeline:
- FreeCAD GUI binary exports individual page PDFs via
TechDrawGui.exportPageAsPdf() - Pages are merged with optional cover page (metadata, TOC, BOM) and instructions
This requires the FreeCAD GUI binary (not freecadcmd). On macOS it runs headlessly without displaying a window. Set FREECAD_GUI_BINARY environment variable if FreeCAD is not in a standard location.
Why use this:
- Keep technical drawings in sync with CAD model
- Embed in documentation
- Version control drawings alongside 3MF
- PDF preserves all annotations, hatching, dimensions, and balloons
Generate publication-ready screenshots (Thingiverse/Printables) using the FreeCAD GUI renderer.
export:
- name: Antenna
source: Antenna.FCStd
bodies: [MainBody, Feed001]
output: prints/Antenna.3mf
screenshots:
enabled: true
output_dir: docs/images/
views: [isometric, front, top]
resolution: [1920, 1080]
format: png
composite: trueNotes:
- Requires the FreeCAD GUI binary (not
freecadcmd). - Screenshot failures are non-fatal to the main export.
Automatically extract parts lists from assemblies:
export:
- name: Antenna
source: Antenna.FCStd
bodies:
- MainBody
- Feed001
output: prints/Antenna.3mf
bom:
source: auto # auto/assembly/spreadsheet/parts
output: docs/Antenna_BOM.csv
fields: # Optional custom fields
- material
- vendor
- priceBOM Sources (tried in order):
-
assembly- FreeCAD 1.0+ native Assembly workbench- Reads assembly tree and counts duplicates
- Most detailed and accurate
-
spreadsheet- FreeCAD Spreadsheet workbench- Reads cells from "BOM" spreadsheet
- Good for manual part lists
-
parts- Fallback to Part/Body objects- Lists all Part and Body objects in document
- Simple but less detailed
Custom Spreadsheet Name:
bom:
source: spreadsheet
spreadsheet_name: "ComponentList" # If not named "BOM"
output: docs/parts.csvWhat gets generated:
- CSV file with columns matching the BOM source:
- Assembly: Columns from BomObject (e.g.,
Index,Name,Description,File Name,Quantity) - Spreadsheet: Columns from spreadsheet cells
- Parts:
label,quantity, plus any custom fields
- Assembly: Columns from BomObject (e.g.,
- Example output (assembly source):
Index,Name,Description,File Name,Quantity 1,Bearing 608,8mm Ball Bearing,Bearing.FCStd,4 2,Housing,Main housing,Housing.FCStd,1
Why use this:
- ✅ Track bill of materials alongside design
- ✅ Pricing and vendor info
- ✅ Auto-count duplicates in assemblies
- ✅ Easy import to spreadsheets for procurement
output_root: generated # Optional: base dir for relative output paths
export:
- name: Example
source: example.FCStdNotes:
output_rootapplies to relative output fields (output,techdraw.output_dir,screenshots.output_dir,bom.output,slicer.output_dir).- Absolute paths are preserved.
sourceremains resolved from project/config location.
techdraw: # Optional: export technical drawings
pages: [] # Which pages to export
# - Empty/omitted = all pages
# - List page labels: ["Drawing1", "Assembly"]
output_dir: docs # Where to save exported files
format: pdf # Currently only 'pdf' supported
instructions: INSTRUCTIONS.md # Optional: markdown file to include in PDF reportscreenshots: # Optional: generate PNG/JPG screenshots
enabled: true # Boolean, or use short form: screenshots: true
output_dir: docs/images # Where to save images
views: [isometric, front, top] # isometric/front/top/right/back/bottom/left
resolution: [1920, 1080] # [width, height]
background: [255, 255, 255, 255] # RGBA (currently best-effort)
format: png # png or jpg
composite: true # true: all bodies together; false: per-bodybom: # Optional: generate bill of materials
source: auto # Where to get BOM data:
# - 'auto' = try assembly → spreadsheet → parts
# - 'assembly' = only Assembly
# - 'spreadsheet' = only Spreadsheet
# - 'parts' = only Part/Body objects
assembly: MainAssembly # (optional) Specific assembly name/label for 'assembly' source
output: docs/bom.csv # CSV output file path
spreadsheet_name: BOM # (optional) Spreadsheet name if not "BOM"
fields: # (optional) Custom property names to extract
- material
- vendor
- price
- dimensionsslicer:
enabled: true
engine: prusa # prusa | orca
binary: /path/to/slicer # optional override; auto-detected on macOS/PATH
output_dir: test_output/gcode # where generated gcode is written
output_name: "{name}_{engine}_{date}.gcode"
run_after_export: true # optional, default true
dry_run: false # optional per-item slicer dry run
# Optional: use slicer settings bundle instead of profile triplet
use_config_bundle: false
config_bundle: profiles/prusa.ini
prusa: # used when engine=prusa
printer_profile: "Original Prusa MINI & MINI+ Input Shaper"
print_profile: "0.20mm STRUCTURAL @MINIIS 0.4 - Flo"
material_profile: "Generic PLA @MINIIS"
extra_args: []
orca: # used when engine=orca
extra_args: []Notes:
- Native-profile mode only: no profile translation between slicers.
- If
templateis set on the export item, profile fields may be omitted. - Without
template, provide either profile fields oruse_config_bundle: true. - For
engine: prusa, if profile overrides are omitted andtemplateis set, slicer settings are loaded fromMetadata/Slic3r_PE.configin the template 3MF via a temporary--loadconfig bundle. - For
engine: orca, iftemplateis set and no explicit config bundle is provided, the same template config is passed via--load-settings. - Prusa binary G-code may not be plain text; use
tools/gcode_bounds.pyto report XY bounds.
Multiple BOMs from Different Assemblies:
bom:
- assembly: MainAssembly
output: docs/main_bom.csv
- assembly: SubAssembly
output: docs/sub_bom.csvexport:
- name: CompleteProject
source: CompleteProject.FCStd
# 3MF export
bodies:
- MainAssembly
- Bracket
output: prints/Project.3mf
template: template_print_settings.3mf
keep_stl: false
# Technical drawings
techdraw:
pages: [] # All TechDraw pages
output_dir: docs
format: pdf
instructions: INSTRUCTIONS.md
# Screenshots for publication
screenshots:
enabled: true
output_dir: docs/images
views: [isometric, front]
# Bill of materials
bom:
source: auto # Auto-detect from assembly
output: docs/bom.csv
fields:
- material
- vendor
- stock_code
# Metadata
metadata:
Project: "CompleteProject"
Version: "2.0"
Author: "Engineering Team"MyProject/
├── .freecad_tools/
│ └── export.yml # ← Your config goes here
└── MyProject.FCStd
Or legacy location:
MyProject/
├── export_config.yml # ← Alternative location
└── MyProject.FCStd
export:
- name: ExportName # Required: used for output filename
source: MyProject.FCStd # Required: FreeCAD file path
# Bodies to export (see options below)
bodies: # Can be:
- Body # - Simple strings
- body: "Body2" # - Objects with transforms
rotation: [0, 0, 45]
position: [10, 0, 0]
# OR leave empty to use marked bodies:
# bodies: []
# Output location (optional, defaults to prints/{name}.3mf)
output: prints/MyExport.3mf
# Preserve printer settings from template (optional)
template: template_print_settings.3mf
# Keep intermediate STL files (optional, default: false)
keep_stl: false
stl_output_dir: prints/stl
# Embed metadata in 3MF (optional)
metadata:
Project: "MyProject"
Version: "1.0"
Author: "Your Name"
Description: "Custom description"
# Git metadata auto-added if availableSetup automatic exports when you push to git:
cd MyProject
# Copy hook config
cp /path/to/freecad_tools/templates/pre-commit-config.yaml.example .pre-commit-config.yaml
# Install pre-commit
pip install pre-commit
pre-commit install --hook-stage pre-push
# Now exports happen automatically on git push!
git add .
git commit -m "Update antenna design"
git push # → Auto-exports to prints/Export the same body multiple times with different positions:
export:
- name: PrintTray
source: Cable.FCStd
bodies:
# Original position
- body: "CableGuide"
position: [0, 0, 0]
# Copy 1: rotated 90°
- body: "CableGuide"
rotation: [0, 0, 90]
position: [25, 0, 0]
# Copy 2: rotated 180°
- body: "CableGuide"
rotation: [0, 0, 180]
position: [50, 0, 0]
# Copy 3: rotated 270°
- body: "CableGuide"
rotation: [0, 0, 270]
position: [75, 0, 0]
output: prints/Cable_Array.3mfUse FreeCAD macros to create multiple versions:
-
In FreeCAD, run macro:
Macros > Execute > generate_variant_configs.py
-
Dialog appears asking for:
- Parameter spreadsheet name
- Parameter names and values
- Number of variations
-
Macro generates
.freecad_tools/macro_config.yml -
Config is reused for consistent variations
Using Configuration Files (skip the dialog):
Create .freecad_tools/macro_config.yml manually:
spreadsheet_label: VariantData
parameters:
- name: PipeDiameter
start: 10.1
stop: 10.3
step: 0.1
- name: HexIndent
values: [0.3, 0.5, 0.7, 0.9]
- name: HexLength
values: [10]When the macro runs, it will load this config automatically (or show the dialog if not found).
The old param1_name / param1_values dialog-style keys are not supported; keep all variant parameters in the
parameters list.
For variant_array_assignment.py, the config can also define the array target and whether to clean stale generated
array links and hidden auto-delete CopyOnChangeGroup objects before assigning variants:
spreadsheet_label: VariantData
array_label: VariantTestArray
cleanup_before_assign: true
cleanup_untagged_copy_groups: true
enable_link_copy_on_change: trueThe macro enables link copy-on-change by default so each array element can keep an independent selected configuration.
Newly-created managed CopyOnChangeGroup objects are tagged with the array name; cleanup_untagged_copy_groups keeps
one-time cleanup behavior for old untagged hidden groups from previous macro runs.
When you specify both a template and metadata in your config, they are merged:
export:
- name: MyProject
source: MyProject.FCStd
bodies: [Body]
output: prints/MyProject.3mf
template: template_print_settings.3mf
metadata:
Project: "MyProject"
QualityLevel: "Draft"How merging works:
- Template 3MF metadata is read (e.g.,
PrinterName,MaterialProfile) - Your export metadata is merged on top
- Export values take precedence over template values for the same key
- Result contains both template and export metadata
Creating a Template:
- Configure your printer settings in PrusaSlicer
- Export/save as
template_print_settings.3mf - Reference in config with
template:key - Keep one template per printer setup (e.g.,
template_prusa_mk3s.3mf)
freecad_tools/
├── README.md # This file (all user documentation)
├── CHANGELOG.md # What's new
├── AGENTS.md # Agent/developer guide
├── TODO.md # Open features & tasks
│
├── tools/ # Python command-line tools
│ ├── export.py # Entry point (user runs this)
│ ├── fc_export.py # FreeCAD integration (runs inside FreeCAD)
│ ├── lib3mf_utils.py # 3MF creation (runs in venv)
│ ├── git_utils.py # Git metadata extraction
│ ├── techdraw_export.py # TechDraw PDF export (runs in FreeCAD GUI)
│ ├── techdraw_pdf.py # PDF merging/cover page (runs in venv)
│ └── bom_utils.py # BOM CSV generation utility
│
├── macros/ # FreeCAD macros
│ ├── macro_helper.py # Utilities for macro development
│ ├── generate_variant_configs.py # Create design variants
│ └── variant_array_assignment.py # Manage array-based variants
│
├── templates/ # Example configurations
│ ├── pre-commit-config.yaml.example # Hook setup template
│ └── export_config.yml.example.yml # Config template
│
├── examples/ # Sample files
│ ├── example.FCStd # Sample FreeCAD document
│ ├── example.3mf # Sample output
│ └── export_config.yml.example.yml # Example config
│
├── .pre-commit-hooks.yaml # Hook definitions for projects
├── pyproject.toml # Python dependencies
└── uv.lock # Locked dependency versions
pip install freecad-toolsThis installs the freecad-export command globally:
freecad-export --help
freecad-export path/to/config.yml-
Clone the repository:
git clone https://github.com/ebirn/freecad_tools.git cd freecad_tools -
Create virtual environment and install dependencies:
uv sync
Or without uv:
python3 -m venv .venv source .venv/bin/activate pip install -e .
-
Verify installation:
freecad-export --help # Or run directly: python3 tools/export.py --help
This installs the freecad-export command globally:
freecad-export --help
freecad-export path/to/config.yml-
Clone the repository:
git clone https://github.com/ebirn/freecad_tools.git cd freecad_tools -
Create virtual environment and install dependencies:
uv sync
Or without uv:
python3 -m venv .venv source .venv/bin/activate pip install -e .
-
Verify installation:
freecad-export --help # Or run directly: python3 tools/export.py --help
- Python: 3.10 or higher
- FreeCAD: v0.20+ (v1.0+ required for Assembly BOM features)
- OS: macOS, Linux, or Windows
- Dependencies: PyYAML, lib3mf, pypdf, reportlab (auto-installed via
uv sync)
For macro developers, macro_helper.py provides utilities:
from macro_helper import show_config_dialog
fields = [
{"name": "param1", "type": "text", "label": "Parameter 1:", "default": "value1"},
{"name": "count", "type": "number", "label": "Count:", "default": 5}
]
config = show_config_dialog(title="My Configuration", fields=fields)from macro_helper import get_object_by_identifier
obj = get_object_by_identifier(doc, "Feed001") # By Label or Namefrom macro_helper import find_exportable_bodies
bodies = find_exportable_bodies(doc) # Bodies with ExportTo3MF=Truefrom macro_helper import load_or_prompt_config
config = load_or_prompt_config(
config_path=".freecad_tools/my_config.yml",
dialog_fields=fields,
dialog_title="My Macro Configuration"
)
# Loads config from file, or shows dialog and saves resultfrom macro_helper import get_body_property, set_body_property
export_flag = get_body_property(obj, "ExportTo3MF")
set_body_property(obj, "ExportTo3MF", True, property_type="App::Bool")The tool can't find your FreeCAD installation:
macOS:
# Check if FreeCAD.app exists
ls /Applications/FreeCAD.app/Contents/Resources/bin/freecadcmdLinux:
which freecadcmdWindows:
where freecadcmd.exeThe body name in config doesn't match:
Solution: Check the exact name in FreeCAD:
- In FreeCAD tree, right-click body → Properties
- Look for "Label" or "Name"
- Use the Label (user-friendly name) in config
Check the export log:
cat fc_export.logCommon issues:
- Invalid FreeCAD file path
- Body has no shape/geometry
- Permissions issue with output directory
Adjust tessellation (mesh quality):
The tool auto-calculates based on object size (0.1% of max dimension). For finer control, create an issue on GitHub.
| Feature | Basic | Advanced |
|---|---|---|
| Export to 3MF | ✅ | ✅ |
| Multiple bodies | ✅ | ✅ |
| Body rotation | ❌ | ✅ |
| Body positioning | ❌ | ✅ |
| Metadata embedding | ❌ | ✅ |
| TechDraw export | ❌ | ✅ |
| Bill of Materials | ❌ | ✅ |
| Git integration | ❌ | ✅ |
| Body marking | ❌ | ✅ |
| Auto-export hooks | ❌ | ✅ |
| Template support | ❌ | ✅ |
| Keep STL files | ❌ | ✅ |
export:
- name: Antenna_Complete
source: Antenna.FCStd
bodies:
# Main radiator - centered, no rotation
- body: "Radiator"
position: [0, 0, 0]
# Feed point - centered, rotated for optimal orientation
- body: "Feed"
rotation: [45, 0, 0]
position: [0, 0, 10]
# Mounting bracket - positioned to side
- body: "Bracket"
rotation: [0, 0, 0]
position: [30, 0, 5]
# Cable guide - rotated for cable routing
- body: "CableGuide"
rotation: [0, 90, 0]
position: [15, 15, 0]
output: prints/Antenna_Complete.3mf
metadata:
Project: "VHF Antenna"
Version: "3.2"
Author: "RF Team"
Description: "10 element Yagi antenna, optimized for 2m band"
Material: "PETG"
Color: "Black"Once configured, exports happen automatically:
$ git push
Auto-exporting antenna design...
✓ Created antenna.3mf (2.4 MB)
✓ Embedded metadata (GitCommit: abc1234, GitBranch: main)
✓ Ready for printing in PrusaSlicer-
Use body Labels, not Names
- Names: "Body", "Body002" (auto-generated, change when bodies reorder)
- Labels: "Feed", "Cover" (user-friendly, stable)
-
Commit your exports
- Keep printed 3MF files in version control
- Track which version was printed
- Revert to previous version if needed
-
Use metadata for traceability
metadata: Version: "1.2" # Update with each design change Date: "2024-04-25" Notes: "First test print successful"
-
Test rotations in FreeCAD first
- Rotate the body manually in FreeCAD
- Note the rotation angles
- Use in config
-
Keep a template 3MF
- Export once from PrusaSlicer with ideal settings
- Save as
template_print_settings.3mf - Reference in config to preserve settings
- Basic Export: Copy example config, run export
- Add Rotation: Add rotation to one body
- Add Metadata: Include project info
- Setup Git Hooks: Automate on push
- Multiple Variants: Export 3-4 design versions
CHANGELOG.md: What's new in each versionexamples/: Sample files to copy from- Issues on GitHub: Ask questions or report bugs
The project has comprehensive unit and integration tests.
Unit Tests (no FreeCAD required, fast):
# Run all unit tests
make test
# or: python3 -m pytest tests/ --ignore=tests/test_fc_export_integration.py
# Run specific test file
python3 -m pytest tests/test_export_config.py -vIntegration Tests (require FreeCAD):
# Run integration tests with FreeCAD
make test-integration
# Run a specific integration test
/FreeCAD.app/Contents/Resources/bin/freecadcmd -c "import sys; sys.path.insert(0, 'tests'); sys.path.insert(0, 'tools'); import pytest; pytest.main(['-v', 'tests/test_fc_export_integration.py'])"All Tests:
make test-unit # Unit tests only
make test-integration # Integration tests only
make test-all # Both unit and integration tests| Target | Description | Requires FreeCAD |
|---|---|---|
test |
All unit tests | No |
test-unit |
Unit tests only | No |
test-integration |
Integration tests only | Yes |
test-all |
Unit + integration | Yes |
Note: Integration tests run via FreeCAD's Python (freecadcmd) and test actual FreeCAD document interactions. They mock the lib3mf subprocess calls, so lib3mf only needs to be installed in your venv, not in FreeCAD's Python.
Generate HTML coverage reports for visual inspection:
# Unit tests with HTML coverage report
python3 -m pytest tests/ --ignore=tests/test_fc_export_integration.py --cov=tools --cov-report=html -v
# Open the report in browser
open htmlcov/index.htmlThe htmlcov/ directory contains interactive coverage reports and is git-ignored.
- Add unit tests in
tests/for pure Python logic (no FreeCAD) - Add integration tests in
tests/test_fc_export_integration.pyfor FreeCAD-dependent code - Use
@skip_if_no_freecaddecorator for tests requiring FreeCAD - See
tests/conftest.pyfor shared fixtures
After each export, quality metrics are automatically collected and logged:
| Metric | Description |
|---|---|
| Vertex Count | Total unique vertices across all meshes |
| Triangle Count | Total triangles across all meshes |
| STL Input Size | Combined size of all input STL files |
| 3MF Output Size | Size of generated 3MF file |
| Per-Body Metrics | Vertex/triangle counts per body (in debug logs) |
The metrics are logged at INFO level after successful exports:
Quality Metrics: 15420 vertices, 30840 triangles, 123456 bytes STL input, 98765 bytes 3MF output
Before tagging a release, run the release readiness checker to verify your project is in a consistent state:
# Quick summary (JSON)
python3 tools/release_validator.py --summary
# Individual checks
python3 tools/release_validator.py --check version
python3 tools/release_validator.py --check changelog
python3 tools/release_validator.py --check tests
python3 tools/release_validator.py --check all
# Generate checksums for release files
python3 tools/release_validator.py --checksums README.md CHANGELOG.md pyproject.tomlThe --summary flag validates:
- Version —
pyproject.tomlhas a version set - Changelog —
CHANGELOG.mdhas an entry matching the version - Unreleased section —
CHANGELOG.mdhas an[Unreleased]section for future changes - Tests — all unit tests pass
The release workflows use these same checks as gates — a tagged release will fail if any check doesn't pass.
Before tagging a release, run the release readiness checker to verify your project is in a consistent state:
# Quick summary (JSON)
python3 tools/release_validator.py --summary
# Individual checks
python3 tools/release_validator.py --check version
python3 tools/release_validator.py --check changelog
python3 tools/release_validator.py --check tests
python3 tools/release_validator.py --check all
# Generate checksums for release files
python3 tools/release_validator.py --checksums README.md CHANGELOG.md pyproject.tomlThe --summary flag validates:
- Version —
pyproject.tomlhas a version set - Changelog —
CHANGELOG.mdhas an entry matching the version - Unreleased section —
CHANGELOG.mdhas an[Unreleased]section for future changes - Tests — all unit tests pass
The release workflows use these same checks as gates — a tagged release will fail if any check doesn't pass.
- Review examples in
examples/directory - Check
CHANGELOG.mdfor recent changes - Check
TODO.mdfor planned features - Open an issue on GitHub
- Note the error message
- Check
fc_export.logfor details - Open an issue with:
- OS and FreeCAD version
- Your config file
- The error message
- Steps to reproduce
See AGENTS.md for development guidelines.
See CHANGELOG.md for complete release history.
Happy Printing! 🖨️
Made with ❤️ for the FreeCAD and 3D printing community.