github/awesome-copilot

freecad-scripts

Expert skill for writing FreeCAD Python scripts, macros, and automation.

Ver código fuente
Documento original del Skill

Contenido del repositorio de origen con títulos, ejemplos, código, tablas, enlaces e imágenes preservados.

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:

python
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

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

python
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:

python
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

python
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

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

idxline = sketch.addGeometry(Part.LineSegment( FreeCAD.Vector(0, 0, 0), FreeCAD.Vector(10, 0, 0))) idxcircle = 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

btnlayout = QtWidgets.QHBoxLayout() self.btnok = QtWidgets.QPushButton("OK") self.btncancel = QtWidgets.QPushButton("Cancel") btnlayout.addWidget(self.btnok) btnlayout.addWidget(self.btncancel) layout.addLayout(btnlayout)

self.btnok.clicked.connect(self.accept) self.btncancel.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", ["MyCustomCommand"]) self.appendMenu("My Menu", ["MyCustomCommand"])

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") qinch = FreeCAD.Units.Quantity("1 in") print(qinch.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()` after modifications
5. **GUI guard**: Wrap GUI-dependent code in `if FreeCAD.GuiUp:` when the script may run headless
6. **Part.show()**: Use `Part.show(shape, "Name")` for quick display, or `doc.addObject("Part::Feature", "Name")` for named persistent objects

## References

### Primary Links

- [Writing Python code](https://wiki.freecad.org/Manual:A_gentle_introduction#Writing_Python_code)
- [Manipulating FreeCAD objects](https://wiki.freecad.org/Manual:A_gentle_introduction#Manipulating_FreeCAD_objects)
- [Vectors and Placements](https://wiki.freecad.org/Manual:A_gentle_introduction#Vectors_and_Placements)
- [Creating and manipulating geometry](https://wiki.freecad.org/Manual:Creating_and_manipulating_geometry)
- [Creating parametric objects](https://wiki.freecad.org/Manual:Creating_parametric_objects)
- [Creating interface tools](https://wiki.freecad.org/Manual:Creating_interface_tools)
- [Python](https://en.wikipedia.org/wiki/Python_%28programming_language%29)
- [Introduction to Python](https://wiki.freecad.org/Introduction_to_Python)
- [Python scripting tutorial](https://wiki.freecad.org/Python_scripting_tutorial)
- [FreeCAD scripting basics](https://wiki.freecad.org/FreeCAD_Scripting_Basics)
- [Gui Command](https://wiki.freecad.org/Gui_Command)

### Bundled Reference Documents

See the [references/](references/) directory for topic-organized guides:

1. [scripting-fundamentals.md](references/scripting-fundamentals.md) — Core scripting, document model, console
2. [geometry-and-shapes.md](references/geometry-and-shapes.md) — Part, Mesh, Sketcher, topology
3. [parametric-objects.md](references/parametric-objects.md) — FeaturePython, properties, scripted objects
4. [gui-and-interface.md](references/gui-and-interface.md) — PySide, dialogs, task panels, Coin3D
5. [workbenches-and-advanced.md](references/workbenches-and-advanced.md) — Workbenches, macros, FEM, Path, recipes
del mismo repositorio

Más Skills

Todos los Skills