freecad-scripts
Expert skill for writing FreeCAD Python scripts, macros, and automation. Use when asked to create FreeCAD models, parametric objects, Part/Mesh/Sketcher scripts, workbench tools, GUI dialogs with PySide, Coin3D scenegraph manipulation, or any FreeCAD Python API task. Covers FreeCAD scripting basics,
By github · 1,144 installs
npx skills add github/awesome-copilot --skill freecad-scripts
Source repository · Upstream listing
FreeCAD Scripts
Expert skill for generating production quality Python scripts for the FreeCAD CAD application. Interprets shorthand, quasi code, and natural language descriptions of 3D modeling tasks and translates them into correct FreeCAD Python API calls.
When to Use This Skill
Writing Python scripts for FreeCAD's built in console or macro system
Creating or manipulating 3D geometry (Part, Mesh, Sketcher, Path, FEM)
Building parametric FeaturePython objects with custom properties
Developing GUI tools using PySide/Qt within FreeCAD
Manipulating the Coin3D scenegraph via Pivy
Creating custom workbenches or Gui Commands
Automating repetitive CAD operations with macros
Converting between mesh and solid representations
Scripting FEM analyses, raytracing, or drawing exports
Prerequisites
FreeCAD installed (0.19+ recommended; 0.21+/1.0+ for latest API)
Python 3.x (bundled with FreeCAD)
For GUI work: PySide2 (bundled with FreeCAD)
For scenegraph: Pivy (bundled with FreeCAD)
FreeCAD Python Environment
FreeCAD embeds a Python interpreter. Scripts run in an environment where these key modules are available:
The FreeCAD Document Model
Core Concepts
Vectors and Placements
Creating and Manipulating Geometry (Part Module)
The Part module wraps OpenCASCADE and provides BRep solid modeling:
Topological Exploration
Mesh Module
Sketcher Module
Create a sketch on XY plane
sketch = doc.addObject("Sketcher::SketchObject", "MySketch")
sketch.Placement = FreeCAD.Placement(
FreeCAD.Vector(0, 0, 0),
FreeCAD.Rotation(0, 0, 0, 1)
)
Add geometry (returns geometry index)
idx line = sketch.addGeometry(Part.LineSegment(
FreeCAD.Vector(0, 0, 0), FreeCAD.Vector(10, 0, 0)))
idx circle = sketch.addGeometry(Part.Circle(
FreeCAD.Vector(5, 5, 0), FreeCAD.Vector(0, 0, 1), 3))
Add constraints
sketch.addConstraint(Sketcher.Constraint("Coincident", 0, 2, 1, 1))
sketch.addConstraint(Sketcher.Constraint("Horizontal", 0))
sketch.addConstraint(Sketcher.Constraint("DistanceX", 0, 1, 0, 2, 10.0))
sketch.addConstraint(Sketcher.Constraint("Radius", 1, 3.0))
sketch.addConstraint(Sketcher.Constraint("Fixed", 0, 1))
Constraint types: Coincident, Horizontal, Vertical, Parallel, Perpendicular,
Tangent, Equal, Symmetric, Distance, DistanceX, DistanceY, Radius, Angle,
Fixed (Block), InternalAlignment
doc.recompute()
python
import Draft
import FreeCAD
2D shapes
line = Draft.makeLine(FreeCAD.Vector(0,0,0), FreeCAD.Vector(10,0,0))
circle = Draft.makeCircle(5)
rect = Draft.makeRectangle(10, 5)
poly = Draft.makePolygon(6, radius=5) hexagon
Operations
moved = Draft.move(obj, FreeCAD.Vector(10, 0, 0), copy=True)
rotated = Draft.rotate(obj, 45, FreeCAD.Vector(0,0,0),
axis=FreeCAD.Vector(0,0,1), copy=True)
scaled = Draft.scale(obj, FreeCAD.Vector(2,2,2), center=FreeCAD.Vector(0,0,0),
copy=True)
offset = Draft.offset(obj, FreeCAD.Vector(1,0,0))
array = Draft.makeArray(obj, FreeCAD.Vector(15,0,0),
FreeCAD.Vector(0,15,0), 3, 3)
python
import FreeCAD
import Part
class MyBox:
"""A custom parametric box."""
def init (self, obj):
obj.Proxy = self
obj.addProperty("App::PropertyLength", "Length", "Dimensions",
"Box length").Length = 10.0
obj.addProperty("App::PropertyLength", "Width", "Dimensions",
"Box width").Width = 10.0
obj.addProperty("App::PropertyLength", "Height", "Dimensions",
"Box height").Height = 10.0
def execute(self, obj):
"""Called on document recompute."""
obj.Shape = Part.makeBox(obj.Length, obj.Width, obj.Height)
def onChanged(self, obj, prop):
"""Called when a property changes."""
pass
def getstate (self):
return None
def setstate (self, state):
return None
class ViewProviderMyBox:
"""View provider for custom icon and display settings."""
def init (self, vobj):
vobj.Proxy = self
def getIcon(self):
return ":/icons/Part Box.svg"
def attach(self, vobj):
self.Object = vobj.Object
def updateData(self, obj, prop):
pass
def onChanged(self, vobj, prop):
pass
def getstate (self):
return None
def setstate (self, state):
return None
Usage
doc = FreeCAD.ActiveDocument or FreeCAD.newDocument("Test")
obj = doc.addObject("Part::FeaturePython", "CustomBox")
MyBox(obj)
ViewProviderMyBox(obj.ViewObject)
doc.recompute()
python
import FreeCAD
import FreeCADGui
class MyCommand:
"""A custom toolbar/menu command."""
def GetResources(self):
return {
"Pixmap": ":/icons/Part Box.svg",
"MenuText": "My Custom Command",
"ToolTip": "Creates a custom box",
"Accel": "Ctrl+Shift+B"
}
def IsActive(self):
return FreeCAD.ActiveDocument is not None
def Activated(self):
Command logic here
FreeCAD.Console.PrintMessage("Command activated\n")
FreeCADGui.addCommand("My CustomCommand", MyCommand())
python
from PySide2 import QtWidgets, QtCore, QtGui
class MyDialog(QtWidgets.QDialog):
def init (self, parent=None):
super(). init (parent or FreeCADGui.getMainWindow())
self.setWindowTitle("My Tool")
self.setMinimumWidth(300)
layout = QtWidgets.QVBoxLayout(self)
Input fields
self.label = QtWidgets.QLabel("Length:")
self.spinbox = QtWidgets.QDoubleSpinBox()
self.spinbox.setRange(0.1, 1000.0)
self.spinbox.setValue(10.0)
self.spinbox.setSuffix(" mm")
form = QtWidgets.QFormLayout()
form.addRow(self.label, self.spinbox)
layout.addLayout(form)
Buttons
btn layout = QtWidgets.QHBoxLayout()
self.btn ok = QtWidgets.QPushButton("OK")
self.btn cancel = QtWidgets.QPushButton("Cancel")
btn layout.addWidget(self.btn ok)
btn layout.addWidget(self.btn cancel)
layout.addLayout(btn layout)
self.btn ok.clicked.connect(self.accept)
self.btn cancel.clicked.connect(self.reject)
Usage
dialog = MyDialog()
if dialog.exec () == QtWidgets.QDialog.Accepted:
length = dialog.spinbox.value()
FreeCAD.Console.PrintMessage(f"Length: {length}\n")
python
class MyTaskPanel:
"""Task panel shown in the left sidebar."""
def init (self):
self.form = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout(self.form)
self.spinbox = QtWidgets.QDoubleSpinBox()
self.spinbox.setValue(10.0)
layout.addWidget(QtWidgets.QLabel("Length:"))
layout.addWidget(self.spinbox)
def accept(self):
Called when user clicks OK
length = self.spinbox.value()
FreeCAD.Console.PrintMessage(f"Accepted: {length}\n")
FreeCADGui.Control.closeDialog()
return True
def reject(self):
FreeCADGui.Control.closeDialog()
return True
def getStandardButtons(self):
return int(QtWidgets.QDialogButtonBox.Ok
QtWidgets.QDialogButtonBox.Cancel)
Show the panel
panel = MyTaskPanel()
FreeCADGui.Control.showDialog(panel)
python
from pivy import coin
import FreeCADGui
Access the scenegraph root
sg = FreeCADGui.ActiveDocument.ActiveView.getSceneGraph()
Add a custom separator with a sphere
sep = coin.SoSeparator()
mat = coin.SoMaterial()
mat.diffuseColor.setValue(1.0, 0.0, 0.0) Red
trans = coin.SoTranslation()
trans.translation.setValue(10, 10, 10)
sphere = coin.SoSphere()
sphere.radius.setValue(2.0)
sep.addChild(mat)
sep.addChild(trans)
sep.addChild(sphere)
sg.addChild(sep)
Remove later
sg.removeChild(sep)
python
import FreeCADGui
class MyWorkbench(FreeCADGui.Workbench):
MenuText = "My Workbench"
ToolTip = "A custom workbench"
Icon = ":/icons/freecad.svg"
def Initialize(self):
"""Called at workbench activation."""
import MyCommands Import your command module
self.appendToolbar("My Tools", ["My CustomCommand"])
self.appendMenu("My Menu", ["My CustomCommand"])
def Activated(self):
pass
def Deactivated(self):
pass
def GetClassName(self):
return "Gui::PythonWorkbench"
FreeCADGui.addWorkbench(MyWorkbench)
python
Standard macro header
coding: utf 8
FreeCAD Macro: MyMacro
Description: Brief description of what the macro does
Author: YourName
Version: 1.0
Date: 2026 04 07
import FreeCAD
import Part
from FreeCAD import Base
Guard for GUI availability
if FreeCAD.GuiUp:
import FreeCADGui
from PySide2 import QtWidgets, QtCore
def main():
doc = FreeCAD.ActiveDocument
if doc is None:
FreeCAD.Console.PrintError("No active document\n")
return
if FreeCAD.GuiUp:
sel = FreeCADGui.Selection.getSelection()
if not sel:
FreeCAD.Console.PrintWarning("No objects selected\n")
... macro logic ...
doc.recompute()
FreeCAD.Console.PrintMessage("Macro completed\n")
if name == " main ":
main()
python
Get selected objects
sel = FreeCADGui.Selection.getSelection() List of objects
sel ex = FreeCADGui.Selection.getSelectionEx() Extended (sub elements)
for selobj in sel ex:
obj = selobj.Object
for sub in selobj.SubElementNames:
print(f"{obj.Name}.{sub}")
shape = obj.getSubObject(sub) Get sub shape
Select programmatically
FreeCADGui.Selection.addSelection(doc.MyBox)
FreeCADGui.Selection.addSelection(doc.MyBox, "Face1")
FreeCADGui.Selection.clearSelection()
python
FreeCAD.Console.PrintMessage("Info message\n")
FreeCAD.Console.PrintWarning("Warning message\n")
FreeCAD.Console.PrintError("Error message\n")
FreeCAD.Console.PrintLog("Debug/log message\n")
python
doc = FreeCAD.ActiveDocument
Create sketch
sketch = doc.addObject("Sketcher::SketchObject", "Sketch")
sketch.addGeometry(Part.LineSegment(FreeCAD.Vector(0,0,0), FreeCAD.Vector(10,0,0)))
sketch.addGeometry(Part.LineSegment(FreeCAD.Vector(10,0,0), FreeCAD.Vector(10,10,0)))
sketch.addGeometry(Part.LineSegment(FreeCAD.Vector(10,10,0), FreeCAD.Vector(0,10,0)))
sketch.addGeometry(Part.LineSegment(FreeCAD.Vector(0,10,0), FreeCAD.Vector(0,0,0)))
Close with coincident constraints
for i in range(3):
sketch.addConstraint(Sketcher.Constraint("Coincident", i, 2, i+1, 1))
sketch.addConstraint(Sketcher.Constraint("Coincident", 3, 2, 0, 1))
Pad (PartDesign)
pad = doc.addObject("PartDesign::Pad", "Pad")
pad.Profile = sketch
pad.Length = 5.0
sketch.Visibility = False
doc.recompute()
python
STEP export
Part.export([doc.MyBox], "/path/to/output.step")
STL export (mesh)
import Mesh
Mesh.export([doc.MyBox], "/path/to/output.stl")
IGES export
Part.export([doc.MyBox], "/path/to/output.iges")
Multiple formats via importlib
import importlib
importlib.import module("importOBJ").export([doc.MyBox], "/path/to/output.obj")
python
FreeCAD uses mm internally
q = FreeCAD.Units.Quantity("10 mm")
q inch = FreeCAD.Units.Quantity("1 in")
print(q inch.getValueAs("mm")) 25.4
Parse user input with units
q = FreeCAD.Units.parseQuantity("2.5 in")
value mm = float(q) Value in mm (internal unit)
Compensation Rules (Quasi Coder Integration)
When interpreting shorthand or quasi code for FreeCAD scripts:
1. Terminology mapping : "box" → Part.makeBox() , "cylinder" → Part.makeCylinder() , "sphere" → Part.makeSphere() , "merge/combine/join" → .fuse() , "subtract/cut/remove" → .cut() , "intersect" → .common() , "round edges/fillet" → .makeFillet() , "bevel/chamfer" → .makeChamfer()
2. Implicit document : If no document handling is mentioned, wrap in standard doc = FreeCAD.ActiveDocument or FreeCAD.newDocument()
3. Units assumption : Default to millimeters unless stated otherwise
4. Recompute : Always call doc.recompute(