Skip to content

Instantly share code, notes, and snippets.

@haliphax
Last active June 22, 2026 04:58
Show Gist options
  • Select an option

  • Save haliphax/87f7828a7842c56c551b1ed77bce4af9 to your computer and use it in GitHub Desktop.

Select an option

Save haliphax/87f7828a7842c56c551b1ed77bce4af9 to your computer and use it in GitHub Desktop.
FreeCAD Macro - Export objects as STL/STEP
"""
BodyExport.FCMacro
Export each selected solid as both STEP and STL into the project file's directory.
Files are named: <project name> - <body name>.<extension>
If no solid objects are selected, top-level results are exported:
- PartDesign::Body objects (unless they are operands of a standalone
boolean result)
- Standalone boolean results (Part::Cut, Part::Fuse, Part::Common)
Usage:
1. Select one or more solid objects in the tree or 3D view
(PartDesign::Body, Part::Cut, Part::Fuse, Part::Common, etc.)
2. Run this macro.
__Name__ = 'BodyExport'
__Comment__ = 'Export selected solids as STEP and STL'
__Author__ = 'haliphax'
__Version__ = '1.8'
__License__ = 'MIT'
__Status__ = 'production'
"""
import FreeCAD
import Part
import os
# ---------------------------------------------------------------------------
# STL export defaults matching the FreeCAD GUI (File -> Export)
# ---------------------------------------------------------------------------
STL_LINEAR_DEFLECTION = 0.5
STL_ANGULAR_DEFLECTION = 0.5235987755982988 # 30 degrees in radians
# TypeIds for boolean operation results
BOOLEAN_TYPE_IDS = frozenset(["Part::Cut", "Part::Fuse", "Part::Common"])
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _project_name_and_dir():
doc = FreeCAD.activeDocument()
if doc is None or not doc.FileName:
raise RuntimeError(
"The document has not been saved yet. Save it before running this macro."
)
doc_dir = os.path.dirname(doc.FileName)
doc_name = os.path.splitext(os.path.basename(doc.FileName))[0]
return doc_name, doc_dir
def _is_body(obj):
return hasattr(obj, "TypeId") and obj.TypeId == "PartDesign::Body"
def _is_boolean_result(obj):
return hasattr(obj, "TypeId") and obj.TypeId in BOOLEAN_TYPE_IDS
def _has_solid_shape(obj):
shape = getattr(obj, "Shape", None)
if shape is None:
return False
try:
return len(shape.Solids) > 0
except Exception:
return False
def _owning_body(obj):
parent = getattr(obj, 'getParentGroup', lambda: None)()
while parent is not None:
if _is_body(parent):
return parent
parent = getattr(parent, 'getParentGroup', lambda: None)()
return None
def _is_in_body(obj):
return _owning_body(obj) is not None
def _is_operand_of(obj, possible_parents):
"""Return True if *obj* is referenced in the InList of any object
in *possible_parents* (i.e. it is a direct dependency of one of them)."""
parent_ids = {id(p) for p in possible_parents}
for caller in getattr(obj, 'InList', []):
if id(caller) in parent_ids:
return True
return False
def _collect_bodies(selection):
"""
Return the list of unique objects to export.
Explicit selection: export exactly what was asked for, no filtering.
Fallback (nothing selected): export top-level results.
"""
raw = []
seen = set()
# --- Explicit selection ------------------------------------------------
if selection:
for obj in selection:
if _is_body(obj):
if id(obj) not in seen:
raw.append(obj)
seen.add(id(obj))
continue
parent_body = _owning_body(obj)
if parent_body is not None:
if id(parent_body) not in seen:
raw.append(parent_body)
seen.add(id(parent_body))
continue
if _has_solid_shape(obj) and id(obj) not in seen:
raw.append(obj)
seen.add(id(obj))
return raw
# --- Fallback ----------------------------------------------------------
doc = FreeCAD.activeDocument()
# 1) Collect all Bodies
all_bodies = []
for o in doc.Objects:
if _is_body(o) and id(o) not in seen:
all_bodies.append(o)
seen.add(id(o))
# 2) Collect all standalone boolean results (not inside a Body)
booleans = []
for o in doc.Objects:
if (_is_boolean_result(o) and _has_solid_shape(o)
and not _is_in_body(o) and id(o) not in seen):
booleans.append(o)
seen.add(id(o))
# 3) Bodies that are operands of a standalone boolean are NOT top-level.
# Use InList: if a Body's InList contains a boolean result, that
# boolean uses this Body as an operand, so the Body is not top-level.
result = []
for body in all_bodies:
if not _is_operand_of(body, booleans):
result.append(body)
# 4) Add the standalone booleans
result.extend(booleans)
return result
def _slugify(name):
safe = []
for ch in name:
if ch in ('/', '\\', ':', '*', '?', '"', '<', '>', '|'):
safe.append('_')
else:
safe.append(ch)
return ''.join(safe)
def _export_stl(obj, stl_path):
import MeshPart
mesh = MeshPart.meshFromShape(
Shape=obj.Shape,
LinearDeflection=STL_LINEAR_DEFLECTION,
AngularDeflection=STL_ANGULAR_DEFLECTION,
Relative=False,
)
mesh.write(stl_path)
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
doc = FreeCAD.activeDocument()
if doc is None:
raise RuntimeError("No active document. Open or create one first.")
proj_name, proj_dir = _project_name_and_dir()
sel = FreeCADGui.Selection.getSelection()
targets = _collect_bodies(sel)
if not targets:
FreeCAD.Console.PrintMessage("No solid objects found. Nothing to export.\n")
return
exported = []
errors = []
for obj in targets:
slug = _slugify(obj.Label)
base_name = proj_name + " - " + slug
step_path = os.path.join(proj_dir, base_name + ".step")
stl_path = os.path.join(proj_dir, base_name + ".stl")
try:
Part.export([obj], step_path)
exported.append(step_path)
except Exception as exc:
errors.append("STEP export failed for '" + obj.Label + "': " + str(exc))
try:
_export_stl(obj, stl_path)
exported.append(stl_path)
except Exception as exc:
errors.append("STL export failed for '" + obj.Label + "': " + str(exc))
if exported:
FreeCAD.Console.PrintMessage("Exported " + str(len(exported)) + " file(s):\n")
for f in exported:
FreeCAD.Console.PrintMessage(" " + f + "\n")
if errors:
FreeCAD.Console.PrintWarning(str(len(errors)) + " error(s) occurred:\n")
for e in errors:
FreeCAD.Console.PrintWarning(" " + e + "\n")
try:
import FreeCADGui
except ImportError:
raise RuntimeError(
"This macro must be run inside the FreeCAD GUI (not headless console mode)."
)
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment