freecad-scripts

Expert skill for writing FreeCAD Python scripts, macros, and automation. Use when asked to create FreeCAD models, parametric objects, Part/Mesh/Sketcher…

INSTALLATION
npx skills add https://github.com/github/awesome-copilot --skill freecad-scripts
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

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:

import FreeCAD          # Core module (also aliased as 'App')

import FreeCADGui       # GUI module (also aliased as 'Gui') — only in GUI mode

import Part             # Part workbench — BRep/OpenCASCADE shapes

import Mesh             # Mesh workbench — triangulated meshes

import Sketcher         # Sketcher workbench — 2D constrained sketches

import Draft            # Draft workbench — 2D drawing tools

import Arch             # Arch/BIM workbench

import Path             # Path/CAM workbench

import FEM              # FEM workbench

import TechDraw         # TechDraw workbench (replaces Drawing)

import BOPTools         # Boolean operations

import CompoundTools    # Compound shape utilities

The FreeCAD Document Model

# Create or access a document

doc = FreeCAD.newDocument("MyDoc")

doc = FreeCAD.ActiveDocument

# Add objects

box = doc.addObject("Part::Box", "MyBox")

box.Length = 10.0

box.Width = 10.0

box.Height = 10.0

# Recompute

doc.recompute()

# Access objects

obj = doc.getObject("MyBox")

obj = doc.MyBox  # Attribute access also works

# Remove objects

doc.removeObject("MyBox")

Core Concepts

Vectors and Placements

import FreeCAD

# Vectors

v1 = FreeCAD.Vector(1, 0, 0)

v2 = FreeCAD.Vector(0, 1, 0)

v3 = v1.cross(v2)          # Cross product

d = v1.dot(v2)              # Dot product

v4 = v1 + v2                # Addition

length = v1.Length           # Magnitude

v_norm = FreeCAD.Vector(v1)

v_norm.normalize()           # In-place normalize

# Rotations

rot = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 45)  # axis, angle(deg)

rot = FreeCAD.Rotation(0, 0, 45)                       # Euler angles (yaw, pitch, roll)

# Placements (position + orientation)

placement = FreeCAD.Placement(

    FreeCAD.Vector(10, 20, 0),    # translation

    FreeCAD.Rotation(0, 0, 45),   # rotation

    FreeCAD.Vector(0, 0, 0)       # center of rotation

)

obj.Placement = placement

# Matrix (4x4 transformation)

import math

mat = FreeCAD.Matrix()

mat.move(FreeCAD.Vector(10, 0, 0))

mat.rotateZ(math.radians(45))

Creating and Manipulating Geometry (Part Module)

The Part module wraps OpenCASCADE and provides BRep solid modeling:

import FreeCAD

import Part

# --- Primitive Shapes ---

box = Part.makeBox(10, 10, 10)               # length, width, height

cyl = Part.makeCylinder(5, 20)               # radius, height

sphere = Part.makeSphere(10)                  # radius

cone = Part.makeCone(5, 2, 10)               # r1, r2, height

torus = Part.makeTorus(10, 2)                 # major_r, minor_r

# --- Wires and Edges ---

edge1 = Part.makeLine((0, 0, 0), (10, 0, 0))

edge2 = Part.makeLine((10, 0, 0), (10, 10, 0))

edge3 = Part.makeLine((10, 10, 0), (0, 0, 0))

wire = Part.Wire([edge1, edge2, edge3])

# Circles and arcs

circle = Part.makeCircle(5)                   # radius

arc = Part.makeCircle(5, FreeCAD.Vector(0, 0, 0),

                       FreeCAD.Vector(0, 0, 1), 0, 180)  # start/end angle

# --- Faces ---

face = Part.Face(wire)                        # From a closed wire

# --- Solids from Faces/Wires ---

extrusion = face.extrude(FreeCAD.Vector(0, 0, 10))       # Extrude

revolved = face.revolve(FreeCAD.Vector(0, 0, 0),

                         FreeCAD.Vector(0, 0, 1), 360)    # Revolve

# --- Boolean Operations ---

fused = box.fuse(cyl)           # Union

cut = box.cut(cyl)              # Subtraction

common = box.common(cyl)        # Intersection

fused_clean = fused.removeSplitter()  # Clean up seams

# --- Fillets and Chamfers ---

filleted = box.makeFillet(1.0, box.Edges)          # radius, edges

chamfered = box.makeChamfer(1.0, box.Edges)        # dist, edges

# --- Loft and Sweep ---

loft = Part.makeLoft([wire1, wire2], True)          # wires, solid

swept = Part.Wire([path_edge]).makePipeShell([profile_wire],

                                              True, False)  # solid, frenet

# --- BSpline Curves ---

from FreeCAD import Vector

points = [Vector(0,0,0), Vector(1,2,0), Vector(3,1,0), Vector(4,3,0)]

bspline = Part.BSplineCurve()

bspline.interpolate(points)

edge = bspline.toShape()

# --- Show in document ---

Part.show(box, "MyBox")    # Quick display (adds to active doc)

# Or explicitly:

doc = FreeCAD.ActiveDocument or FreeCAD.newDocument()

obj = doc.addObject("Part::Feature", "MyShape")

obj.Shape = box

doc.recompute()

Topological Exploration

shape = obj.Shape

# Access sub-elements

shape.Vertexes    # List of Vertex objects

shape.Edges       # List of Edge objects

shape.Wires       # List of Wire objects

shape.Faces       # List of Face objects

shape.Shells      # List of Shell objects

shape.Solids      # List of Solid objects

# Bounding box

bb = shape.BoundBox

print(bb.XMin, bb.XMax, bb.YMin, bb.YMax, bb.ZMin, bb.ZMax)

print(bb.Center)

# Properties

shape.Volume

shape.Area

shape.Length       # For edges/wires

face.Surface       # Underlying geometric surface

edge.Curve         # Underlying geometric curve

# Shape type

shape.ShapeType    # "Solid", "Shell", "Face", "Wire", "Edge", "Vertex", "Compound"

Mesh Module

import Mesh

# Create mesh from vertices and facets

mesh = Mesh.Mesh()

mesh.addFacet(

    0.0, 0.0, 0.0,   # vertex 1

    1.0, 0.0, 0.0,   # vertex 2

    0.0, 1.0, 0.0    # vertex 3

)

# Import/Export

mesh = Mesh.Mesh("/path/to/file.stl")

mesh.write("/path/to/output.stl")

# Convert Part shape to Mesh

import Part

import MeshPart

shape = Part.makeBox(1, 1, 1)

mesh = MeshPart.meshFromShape(Shape=shape, LinearDeflection=0.1,

                                AngularDeflection=0.5)

# Convert Mesh to Part shape

shape = Part.Shape()

shape.makeShapeFromMesh(mesh.Topology, 0.05)  # tolerance

solid = Part.makeSolid(shape)

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()

### Draft Module

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)


## Creating Parametric Objects (FeaturePython)

FeaturePython objects are custom parametric objects with properties that trigger recomputation:

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()


### Common Property Types

Property Type
Python Type
Description

`App::PropertyBool`
`bool`
Boolean

`App::PropertyInteger`
`int`
Integer

`App::PropertyFloat`
`float`
Float

`App::PropertyString`
`str`
String

`App::PropertyLength`
`float` (units)
Length with units

`App::PropertyAngle`
`float` (deg)
Angle in degrees

`App::PropertyVector`
`FreeCAD.Vector`
3D vector

`App::PropertyPlacement`
`FreeCAD.Placement`
Position + rotation

`App::PropertyLink`
object ref
Link to another object

`App::PropertyLinkList`
list of refs
Links to multiple objects

`App::PropertyEnumeration`
`list`/`str`
Dropdown selection

`App::PropertyFile`
`str`
File path

`App::PropertyColor`
`tuple`
RGB color (0.0-1.0)

`App::PropertyPythonObject`
any
Serializable Python object

## Creating GUI Tools

### Gui Commands

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())


### PySide Dialogs

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")


### Task Panel (Recommended for FreeCAD integration)

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)


## Coin3D Scenegraph (Pivy)

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)


## Custom Workbench Creation

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)


## Macro Best Practices

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()


### Selection Handling

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()


### Console Output

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")


## Common Patterns

### Parametric Pad from Sketch

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()


### Export Shapes

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")


### Units and Quantities

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)

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card