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(