Installation

Install with CLI Recommended
gh skills-hub install freecad-scripts

Don't have the extension? Run gh extension install samueltauil/skills-hub first.

Download and extract to your repository:

.github/skills/freecad-scripts/

Extract the ZIP to .github/skills/ in your repo. The folder name must match freecad-scripts for Copilot to auto-discover it.

Skill Files (6)

SKILL.md 20.8 KB
---
name: freecad-scripts
description: '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, geometry creation, FeaturePython objects, interface tools, and macro development.'
---

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

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

## Creating Parametric Objects (FeaturePython)

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

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

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

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

### PySide Dialogs

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

### Task Panel (Recommended for FreeCAD integration)

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

## Coin3D Scenegraph (Pivy)

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

## Custom Workbench Creation

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

## Macro Best Practices

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

### Selection Handling

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

### Console Output

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

## Common Patterns

### Parametric Pad from Sketch

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

### Export Shapes

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

### Units and Quantities

```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()` 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
references/
geometry-and-shapes.md 8.6 KB
# FreeCAD Geometry and Shapes

Reference guide for creating and manipulating geometry in FreeCAD using the Part, Mesh, and Sketcher modules.

## Official Wiki References

- [Creating and manipulating geometry](https://wiki.freecad.org/Manual:Creating_and_manipulating_geometry)
- [Part scripting](https://wiki.freecad.org/Part_scripting)
- [Topological data scripting](https://wiki.freecad.org/Topological_data_scripting)
- [Mesh scripting](https://wiki.freecad.org/Mesh_Scripting)
- [Mesh to Part conversion](https://wiki.freecad.org/Mesh_to_Part)
- [Sketcher scripting](https://wiki.freecad.org/Sketcher_scripting)
- [Drawing API example](https://wiki.freecad.org/Drawing_API_example)
- [Part: Create a ball bearing I](https://wiki.freecad.org/Scripted_Parts:_Ball_Bearing_-_Part_1)
- [Part: Create a ball bearing II](https://wiki.freecad.org/Scripted_Parts:_Ball_Bearing_-_Part_2)
- [Line drawing function](https://wiki.freecad.org/Line_drawing_function)

## Part Module — Shape Hierarchy

OpenCASCADE topology levels (bottom to top):

```
Vertex → Edge → Wire → Face → Shell → Solid → CompSolid → Compound
```

Each level contains the levels below it.

## Primitive Shapes

```python
import Part
import FreeCAD as App

# Boxes
box = Part.makeBox(length, width, height)
box = Part.makeBox(10, 20, 30, App.Vector(0,0,0), App.Vector(0,0,1))

# Cylinders
cyl = Part.makeCylinder(radius, height)
cyl = Part.makeCylinder(5, 20, App.Vector(0,0,0), App.Vector(0,0,1), 360)

# Cones
cone = Part.makeCone(r1, r2, height)

# Spheres
sph = Part.makeSphere(radius)
sph = Part.makeSphere(10, App.Vector(0,0,0), App.Vector(0,0,1), -90, 90, 360)

# Torus
tor = Part.makeTorus(majorR, minorR)

# Planes (infinite → bounded face)
plane = Part.makePlane(length, width)
plane = Part.makePlane(10, 10, App.Vector(0,0,0), App.Vector(0,0,1))

# Helix
helix = Part.makeHelix(pitch, height, radius)

# Wedge
wedge = Part.makeWedge(xmin, ymin, zmin, z2min, x2min,
                        xmax, ymax, zmax, z2max, x2max)
```

## Curves and Edges

```python
# Line segment
line = Part.makeLine((0,0,0), (10,0,0))
line = Part.LineSegment(App.Vector(0,0,0), App.Vector(10,0,0)).toShape()

# Circle (full)
circle = Part.makeCircle(radius)
circle = Part.makeCircle(5, App.Vector(0,0,0), App.Vector(0,0,1))

# Arc (partial circle)
arc = Part.makeCircle(5, App.Vector(0,0,0), App.Vector(0,0,1), 0, 180)

# Arc through 3 points
arc3 = Part.Arc(App.Vector(0,0,0), App.Vector(5,5,0), App.Vector(10,0,0)).toShape()

# Ellipse
ellipse = Part.Ellipse(App.Vector(0,0,0), 10, 5).toShape()

# BSpline curve
points = [App.Vector(0,0,0), App.Vector(2,3,0), App.Vector(5,1,0), App.Vector(8,4,0)]
bspline = Part.BSplineCurve()
bspline.interpolate(points)
edge = bspline.toShape()

# BSpline with control points (approximate)
bspline2 = Part.BSplineCurve()
bspline2.buildFromPoles(points)
edge2 = bspline2.toShape()

# Bezier curve
bezier = Part.BezierCurve()
bezier.setPoles([App.Vector(0,0,0), App.Vector(3,5,0),
                  App.Vector(7,5,0), App.Vector(10,0,0)])
edge3 = bezier.toShape()
```

## Wires, Faces, and Solids

```python
# Wire from edges
wire = Part.Wire([edge1, edge2, edge3])   # edges must connect end-to-end

# Wire by sorting edges
wire = Part.Wire(Part.__sortEdges__([edges_list]))

# Face from wire (must be closed and planar, or a surface)
face = Part.Face(wire)

# Face from multiple wires (first = outer, rest = holes)
face = Part.Face([outer_wire, hole_wire1, hole_wire2])

# Shell from faces
shell = Part.Shell([face1, face2, face3])

# Solid from shell (must be closed)
solid = Part.Solid(shell)

# Compound (group shapes without merging)
compound = Part.Compound([shape1, shape2, shape3])
```

## Shape Operations

```python
# Boolean operations
union = shape1.fuse(shape2)
diff = shape1.cut(shape2)
inter = shape1.common(shape2)

# Multi-fuse / multi-cut
multi_fuse = shape1.multiFuse([shape2, shape3, shape4])

# Clean seam edges after boolean
clean = union.removeSplitter()

# Fillet (round edges)
filleted = solid.makeFillet(radius, solid.Edges)
filleted = solid.makeFillet(radius, [solid.Edges[0], solid.Edges[3]])

# Chamfer
chamfered = solid.makeChamfer(distance, solid.Edges)
chamfered = solid.makeChamfer(dist1, dist2, [solid.Edges[0]])  # asymmetric

# Offset (shell/thicken)
offset = solid.makeOffsetShape(offset_distance, tolerance)
thick = solid.makeThickness([face_to_remove], thickness, tolerance)

# Section (intersection curve of solid with plane)
section = solid.section(Part.makePlane(100, 100, App.Vector(0,0,5)))
```

## Extrude, Revolve, Loft, Sweep

```python
# Extrude face or wire
extruded = face.extrude(App.Vector(0, 0, 10))    # direction vector

# Revolve
revolved = face.revolve(
    App.Vector(0, 0, 0),     # center
    App.Vector(0, 1, 0),     # axis
    360                       # angle (degrees)
)

# Loft between wires/profiles
loft = Part.makeLoft([wire1, wire2, wire3], True)   # solid=True

# Sweep (pipe)
sweep = Part.Wire([path_edge]).makePipe(profile_wire)

# Sweep with Frenet frame
sweep = Part.Wire([path_edge]).makePipeShell(
    [profile_wire],
    True,    # make solid
    False    # use Frenet frame
)
```

## Topological Exploration

```python
shape = obj.Shape

# Sub-element access
shape.Vertexes          # [Vertex, ...]
shape.Edges             # [Edge, ...]
shape.Wires             # [Wire, ...]
shape.Faces             # [Face, ...]
shape.Shells            # [Shell, ...]
shape.Solids            # [Solid, ...]

# Vertex properties
v = shape.Vertexes[0]
v.Point                 # FreeCAD.Vector — the 3D coordinate

# Edge properties
e = shape.Edges[0]
e.Length
e.Curve                 # underlying geometric curve (Line, Circle, BSpline, ...)
e.Vertexes              # start and end vertices
e.firstVertex()         # first Vertex
e.lastVertex()          # last Vertex
e.tangentAt(0.5)        # tangent at parameter
e.valueAt(0.5)          # point at parameter
e.parameterAt(vertex)   # parameter at vertex

# Face properties
f = shape.Faces[0]
f.Area
f.Surface               # underlying geometric surface (Plane, Cylinder, ...)
f.CenterOfMass
f.normalAt(0.5, 0.5)    # normal at (u, v) parameter
f.Wires                 # bounding wires
f.OuterWire             # or Wires[0]

# Bounding box
bb = shape.BoundBox
bb.XMin, bb.XMax, bb.YMin, bb.YMax, bb.ZMin, bb.ZMax
bb.Center, bb.DiagonalLength
bb.XLength, bb.YLength, bb.ZLength

# Shape properties
shape.Volume
shape.Area
shape.CenterOfMass
shape.ShapeType         # "Solid", "Compound", "Face", etc.
shape.isValid()
shape.isClosed()
```

## Sketcher Constraints Reference

| Constraint | Syntax | Description |
|---|---|---|
| Coincident | `("Coincident", geo1, pt1, geo2, pt2)` | Points coincide |
| Horizontal | `("Horizontal", geo)` | Line is horizontal |
| Vertical | `("Vertical", geo)` | Line is vertical |
| Parallel | `("Parallel", geo1, geo2)` | Lines are parallel |
| Perpendicular | `("Perpendicular", geo1, geo2)` | Lines are perpendicular |
| Tangent | `("Tangent", geo1, geo2)` | Curves are tangent |
| Equal | `("Equal", geo1, geo2)` | Equal length/radius |
| Symmetric | `("Symmetric", geo1, pt1, geo2, pt2, geoLine)` | Symmetric about line |
| Distance | `("Distance", geo1, pt1, geo2, pt2, value)` | Distance between points |
| DistanceX | `("DistanceX", geo, pt1, pt2, value)` | Horizontal distance |
| DistanceY | `("DistanceY", geo, pt1, pt2, value)` | Vertical distance |
| Radius | `("Radius", geo, value)` | Circle/arc radius |
| Angle | `("Angle", geo1, geo2, value)` | Angle between lines |
| Fixed | `("Fixed", geo)` | Lock geometry |

Point indices: `1` = start, `2` = end, `3` = center (circles/arcs).
External geometry index: `-1` = X axis, `-2` = Y axis.

## Mesh Operations

```python
import Mesh

# Create from file
mesh = Mesh.Mesh("/path/to/model.stl")

# Create from topology (vertices + facets)
verts = [[0,0,0], [10,0,0], [10,10,0], [0,10,0], [5,5,10]]
facets = [[0,1,4], [1,2,4], [2,3,4], [3,0,4], [0,1,2], [0,2,3]]
mesh = Mesh.Mesh([verts[f[0]] + verts[f[1]] + verts[f[2]] for f in facets])

# Mesh properties
mesh.CountPoints
mesh.CountFacets
mesh.Volume
mesh.Area
mesh.isSolid()

# Mesh operations
mesh.unite(mesh2)       # Boolean union
mesh.intersect(mesh2)   # Boolean intersection
mesh.difference(mesh2)  # Boolean difference
mesh.offset(1.0)        # Offset surface
mesh.smooth()           # Laplacian smoothing

# Export
mesh.write("/path/to/output.stl")
mesh.write("/path/to/output.obj")

# Convert Part → Mesh
import MeshPart
mesh = MeshPart.meshFromShape(
    Shape=part_shape,
    LinearDeflection=0.1,
    AngularDeflection=0.523599,  # ~30 degrees
    Relative=False
)

# Convert Mesh → Part
import Part
tolerance = 0.05
shape = Part.Shape()
shape.makeShapeFromMesh(mesh.Topology, tolerance)
solid = Part.makeSolid(shape)
```
gui-and-interface.md 11.0 KB
# FreeCAD GUI and Interface

Reference guide for building FreeCAD user interfaces: PySide/Qt dialogs, task panels, Gui Commands, Coin3D scenegraph via Pivy.

## Official Wiki References

- [Creating interface tools](https://wiki.freecad.org/Manual:Creating_interface_tools)
- [Gui Command](https://wiki.freecad.org/Gui_Command)
- [Define a command](https://wiki.freecad.org/Command)
- [PySide](https://wiki.freecad.org/PySide)
- [PySide beginner examples](https://wiki.freecad.org/PySide_Beginner_Examples)
- [PySide intermediate examples](https://wiki.freecad.org/PySide_Intermediate_Examples)
- [PySide advanced examples](https://wiki.freecad.org/PySide_Advanced_Examples)
- [PySide usage snippets](https://wiki.freecad.org/PySide_usage_snippets)
- [Interface creation](https://wiki.freecad.org/Interface_creation)
- [Dialog creation](https://wiki.freecad.org/Dialog_creation)
- [Dialog creation with various widgets](https://wiki.freecad.org/Dialog_creation_with_various_widgets)
- [Dialog creation reading and writing files](https://wiki.freecad.org/Dialog_creation_reading_and_writing_files)
- [Dialog creation setting colors](https://wiki.freecad.org/Dialog_creation_setting_colors)
- [Dialog creation image and animated GIF](https://wiki.freecad.org/Dialog_creation_image_and_animated_GIF)
- [Qt Example](https://wiki.freecad.org/Qt_Example)
- [3D view](https://wiki.freecad.org/3D_view)
- [The Coin scenegraph](https://wiki.freecad.org/Scenegraph)
- [Pivy](https://wiki.freecad.org/Pivy)

## Gui Command

The standard way to add toolbar buttons and menu items in FreeCAD:

```python
import FreeCAD
import FreeCADGui

class MyCommand:
    """A registered FreeCAD command."""

    def GetResources(self):
        return {
            "Pixmap": ":/icons/Part_Box.svg",    # Icon (built-in or custom path)
            "MenuText": "My Command",
            "ToolTip": "Does something useful",
            "Accel": "Ctrl+Shift+M",             # Keyboard shortcut
            "CmdType": "ForEdit"                  # Optional: ForEdit, Alter, etc.
        }

    def IsActive(self):
        """Return True if command should be enabled."""
        return FreeCAD.ActiveDocument is not None

    def Activated(self):
        """Called when the command is triggered."""
        FreeCAD.Console.PrintMessage("Command activated!\n")
        # Open a task panel:
        panel = MyTaskPanel()
        FreeCADGui.Control.showDialog(panel)

# Register the command (name must be unique)
FreeCADGui.addCommand("My_Command", MyCommand())
```

## Task Panel (Sidebar Integration)

Task panels appear in FreeCAD's left sidebar — the preferred way to build interactive tools:

```python
import FreeCAD
import FreeCADGui
from PySide2 import QtWidgets, QtCore

class MyTaskPanel:
    """Task panel for the sidebar."""

    def __init__(self):
        # Build the widget
        self.form = QtWidgets.QWidget()
        self.form.setWindowTitle("My Tool")
        layout = QtWidgets.QVBoxLayout(self.form)

        # Input widgets
        self.length_spin = QtWidgets.QDoubleSpinBox()
        self.length_spin.setRange(0.1, 10000.0)
        self.length_spin.setValue(10.0)
        self.length_spin.setSuffix(" mm")
        self.length_spin.setDecimals(2)

        self.width_spin = QtWidgets.QDoubleSpinBox()
        self.width_spin.setRange(0.1, 10000.0)
        self.width_spin.setValue(10.0)
        self.width_spin.setSuffix(" mm")

        self.height_spin = QtWidgets.QDoubleSpinBox()
        self.height_spin.setRange(0.1, 10000.0)
        self.height_spin.setValue(5.0)
        self.height_spin.setSuffix(" mm")

        self.fillet_check = QtWidgets.QCheckBox("Apply fillet")

        # Form layout
        form_layout = QtWidgets.QFormLayout()
        form_layout.addRow("Length:", self.length_spin)
        form_layout.addRow("Width:", self.width_spin)
        form_layout.addRow("Height:", self.height_spin)
        form_layout.addRow(self.fillet_check)
        layout.addLayout(form_layout)

        # Live preview on value change
        self.length_spin.valueChanged.connect(self._preview)
        self.width_spin.valueChanged.connect(self._preview)
        self.height_spin.valueChanged.connect(self._preview)

    def _preview(self):
        """Update preview in 3D view."""
        pass  # Build and display temporary shape

    def accept(self):
        """Called when user clicks OK."""
        import Part
        doc = FreeCAD.ActiveDocument
        shape = Part.makeBox(
            self.length_spin.value(),
            self.width_spin.value(),
            self.height_spin.value()
        )
        Part.show(shape, "MyBox")
        doc.recompute()
        FreeCADGui.Control.closeDialog()
        return True

    def reject(self):
        """Called when user clicks Cancel."""
        FreeCADGui.Control.closeDialog()
        return True

    def getStandardButtons(self):
        """Which buttons to show."""
        return int(QtWidgets.QDialogButtonBox.Ok |
                   QtWidgets.QDialogButtonBox.Cancel)

    def isAllowedAlterSelection(self):
        return True

    def isAllowedAlterView(self):
        return True

    def isAllowedAlterDocument(self):
        return True

# Show:
# FreeCADGui.Control.showDialog(MyTaskPanel())
```

### Task Panel with Multiple Widgets (Multi-Form)

```python
class MultiFormPanel:
    def __init__(self):
        self.form = [self._buildPage1(), self._buildPage2()]

    def _buildPage1(self):
        w = QtWidgets.QWidget()
        w.setWindowTitle("Page 1")
        # ... add widgets ...
        return w

    def _buildPage2(self):
        w = QtWidgets.QWidget()
        w.setWindowTitle("Page 2")
        # ... add widgets ...
        return w
```

## Standalone PySide Dialogs

```python
import FreeCAD
import FreeCADGui
from PySide2 import QtWidgets, QtCore, QtGui

class MyDialog(QtWidgets.QDialog):
    def __init__(self, parent=None):
        super().__init__(parent or (FreeCADGui.getMainWindow() if FreeCAD.GuiUp else None))
        self.setWindowTitle("My Dialog")
        self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)

        layout = QtWidgets.QVBoxLayout(self)

        # Combo box
        self.combo = QtWidgets.QComboBox()
        self.combo.addItems(["Option A", "Option B", "Option C"])
        layout.addWidget(self.combo)

        # Slider
        self.slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
        self.slider.setRange(1, 100)
        self.slider.setValue(50)
        layout.addWidget(self.slider)

        # Text input
        self.line_edit = QtWidgets.QLineEdit()
        self.line_edit.setPlaceholderText("Enter a name...")
        layout.addWidget(self.line_edit)

        # Button box
        buttons = QtWidgets.QDialogButtonBox(
            QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel)
        buttons.accepted.connect(self.accept)
        buttons.rejected.connect(self.reject)
        layout.addWidget(buttons)
```

### Loading a .ui File

```python
import os
from PySide2 import QtWidgets, QtUiTools, QtCore

def loadUiFile(ui_path):
    """Load a Qt Designer .ui file."""
    loader = QtUiTools.QUiLoader()
    file = QtCore.QFile(ui_path)
    file.open(QtCore.QFile.ReadOnly)
    widget = loader.load(file)
    file.close()
    return widget

# In a task panel:
class UiTaskPanel:
    def __init__(self):
        ui_path = os.path.join(os.path.dirname(__file__), "panel.ui")
        self.form = loadUiFile(ui_path)
        # Access widgets by objectName set in Qt Designer
        self.form.myButton.clicked.connect(self._onButton)
```

### File Dialogs

```python
# Open file
path, _ = QtWidgets.QFileDialog.getOpenFileName(
    FreeCADGui.getMainWindow(),
    "Open File",
    "",
    "STEP files (*.step *.stp);;All files (*)"
)

# Save file
path, _ = QtWidgets.QFileDialog.getSaveFileName(
    FreeCADGui.getMainWindow(),
    "Save File",
    "",
    "STL files (*.stl);;All files (*)"
)

# Select directory
path = QtWidgets.QFileDialog.getExistingDirectory(
    FreeCADGui.getMainWindow(),
    "Select Directory"
)
```

### Message Boxes

```python
QtWidgets.QMessageBox.information(None, "Info", "Operation completed.")
QtWidgets.QMessageBox.warning(None, "Warning", "Something may be wrong.")
QtWidgets.QMessageBox.critical(None, "Error", "An error occurred.")

result = QtWidgets.QMessageBox.question(
    None, "Confirm", "Are you sure?",
    QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No
)
if result == QtWidgets.QMessageBox.Yes:
    pass  # proceed
```

### Input Dialogs

```python
text, ok = QtWidgets.QInputDialog.getText(None, "Input", "Enter name:")
value, ok = QtWidgets.QInputDialog.getDouble(None, "Input", "Value:", 10.0, 0, 1000, 2)
choice, ok = QtWidgets.QInputDialog.getItem(None, "Choose", "Select:", ["A","B","C"], 0, False)
```

## Coin3D / Pivy Scenegraph

FreeCAD's 3D view uses Coin3D (Open Inventor). Pivy provides Python bindings.

```python
from pivy import coin
import FreeCADGui

# Get the scenegraph root
sg = FreeCADGui.ActiveDocument.ActiveView.getSceneGraph()

# --- Basic shapes ---
sep = coin.SoSeparator()

# Material (color)
mat = coin.SoMaterial()
mat.diffuseColor.setValue(0.0, 0.8, 0.2)  # RGB 0-1
mat.transparency.setValue(0.3)             # 0=opaque, 1=invisible

# Transform
transform = coin.SoTransform()
transform.translation.setValue(10, 0, 0)
transform.rotation.setValue(coin.SbVec3f(0,0,1), 0.785)  # axis, angle(rad)
transform.scaleFactor.setValue(2, 2, 2)

# Shapes
sphere = coin.SoSphere()
sphere.radius.setValue(3.0)

cube = coin.SoCube()
cube.width.setValue(5)
cube.height.setValue(5)
cube.depth.setValue(5)

cylinder = coin.SoCylinder()
cylinder.radius.setValue(2)
cylinder.height.setValue(10)

# Assemble
sep.addChild(mat)
sep.addChild(transform)
sep.addChild(sphere)
sg.addChild(sep)

# --- Lines ---
line_sep = coin.SoSeparator()
coords = coin.SoCoordinate3()
coords.point.setValues(0, 3, [[0,0,0], [10,0,0], [10,10,0]])
line_set = coin.SoLineSet()
line_set.numVertices.setValue(3)
line_sep.addChild(coords)
line_sep.addChild(line_set)
sg.addChild(line_sep)

# --- Points ---
point_sep = coin.SoSeparator()
style = coin.SoDrawStyle()
style.pointSize.setValue(5)
coords = coin.SoCoordinate3()
coords.point.setValues(0, 3, [[0,0,0], [5,5,0], [10,0,0]])
points = coin.SoPointSet()
point_sep.addChild(style)
point_sep.addChild(coords)
point_sep.addChild(points)
sg.addChild(point_sep)

# --- Text ---
text_sep = coin.SoSeparator()
trans = coin.SoTranslation()
trans.translation.setValue(0, 0, 5)
font = coin.SoFont()
font.name.setValue("Arial")
font.size.setValue(16)
text = coin.SoText2()       # 2D screen-aligned text
text.string.setValue("Hello")
text_sep.addChild(trans)
text_sep.addChild(font)
text_sep.addChild(text)
sg.addChild(text_sep)

# --- Cleanup ---
sg.removeChild(sep)
sg.removeChild(line_sep)
```

## View Manipulation

```python
view = FreeCADGui.ActiveDocument.ActiveView

# Camera operations
view.viewIsometric()
view.viewFront()
view.viewTop()
view.viewRight()
view.fitAll()
view.setCameraOrientation(FreeCAD.Rotation(0, 0, 0))
view.setCameraType("Perspective")   # or "Orthographic"

# Save image
view.saveImage("/path/to/screenshot.png", 1920, 1080, "White")

# Get camera info
cam = view.getCameraNode()
```
parametric-objects.md 10.6 KB
# FreeCAD Parametric Objects

Reference guide for creating FeaturePython objects, scripted objects, properties, view providers, and serialization.

## Official Wiki References

- [Creating parametric objects](https://wiki.freecad.org/Manual:Creating_parametric_objects)
- [Create a FeaturePython object part I](https://wiki.freecad.org/Create_a_FeaturePython_object_part_I)
- [Create a FeaturePython object part II](https://wiki.freecad.org/Create_a_FeaturePython_object_part_II)
- [Scripted objects](https://wiki.freecad.org/Scripted_objects)
- [Scripted objects saving attributes](https://wiki.freecad.org/Scripted_objects_saving_attributes)
- [Scripted objects migration](https://wiki.freecad.org/Scripted_objects_migration)
- [Scripted objects with attachment](https://wiki.freecad.org/Scripted_objects_with_attachment)
- [Viewprovider](https://wiki.freecad.org/Viewprovider)
- [Custom icon in tree view](https://wiki.freecad.org/Custom_icon_in_tree_view)
- [Properties](https://wiki.freecad.org/Property)
- [PropertyLink: InList and OutList](https://wiki.freecad.org/PropertyLink:_InList_and_OutList)
- [FeaturePython methods](https://wiki.freecad.org/FeaturePython_methods)

## FeaturePython Object — Complete Template

```python
import FreeCAD
import Part

class MyParametricObject:
    """Proxy class for a custom parametric object."""

    def __init__(self, obj):
        """Initialize and add properties."""
        obj.Proxy = self
        self.Type = "MyParametricObject"

        # Add custom properties
        obj.addProperty("App::PropertyLength", "Length", "Dimensions",
                         "The length of the object").Length = 10.0
        obj.addProperty("App::PropertyLength", "Width", "Dimensions",
                         "The width of the object").Width = 10.0
        obj.addProperty("App::PropertyLength", "Height", "Dimensions",
                         "The height of the object").Height = 5.0
        obj.addProperty("App::PropertyBool", "Chamfered", "Options",
                         "Apply chamfer to edges").Chamfered = False
        obj.addProperty("App::PropertyLength", "ChamferSize", "Options",
                         "Size of chamfer").ChamferSize = 1.0

    def execute(self, obj):
        """Called when the document is recomputed. Build the shape here."""
        shape = Part.makeBox(obj.Length, obj.Width, obj.Height)
        if obj.Chamfered and obj.ChamferSize > 0:
            shape = shape.makeChamfer(obj.ChamferSize, shape.Edges)
        obj.Shape = shape

    def onChanged(self, obj, prop):
        """Called when any property changes."""
        if prop == "Chamfered":
            # Show/hide ChamferSize based on Chamfered toggle
            if obj.Chamfered:
                obj.setPropertyStatus("ChamferSize", "-Hidden")
            else:
                obj.setPropertyStatus("ChamferSize", "Hidden")

    def onDocumentRestored(self, obj):
        """Called when the document is loaded. Re-initialize if needed."""
        self.Type = "MyParametricObject"

    def __getstate__(self):
        """Serialize the proxy (for saving .FCStd)."""
        return {"Type": self.Type}

    def __setstate__(self, state):
        """Deserialize the proxy (for loading .FCStd)."""
        if state:
            self.Type = state.get("Type", "MyParametricObject")
```

## ViewProvider — Complete Template

```python
import FreeCADGui
from pivy import coin

class ViewProviderMyObject:
    """Controls how the object appears in the 3D view and tree."""

    def __init__(self, vobj):
        vobj.Proxy = self
        # Add view properties if needed
        # vobj.addProperty("App::PropertyColor", "Color", "Display", "Object color")

    def attach(self, vobj):
        """Called when the view provider is attached to the view object."""
        self.Object = vobj.Object
        self.standard = coin.SoGroup()
        vobj.addDisplayMode(self.standard, "Standard")

    def getDisplayModes(self, vobj):
        """Return available display modes."""
        return ["Standard"]

    def getDefaultDisplayMode(self):
        """Return the default display mode."""
        return "Standard"

    def setDisplayMode(self, mode):
        return mode

    def getIcon(self):
        """Return the icon path for the tree view."""
        return ":/icons/Part_Box.svg"
        # Or return an XPM string, or path to a .svg/.png file

    def updateData(self, obj, prop):
        """Called when the model object's data changes."""
        pass

    def onChanged(self, vobj, prop):
        """Called when a view property changes."""
        pass

    def doubleClicked(self, vobj):
        """Called on double-click in the tree."""
        # Open a task panel, for example
        return True

    def setupContextMenu(self, vobj, menu):
        """Add items to the right-click context menu."""
        action = menu.addAction("My Action")
        action.triggered.connect(lambda: self._myAction(vobj))

    def _myAction(self, vobj):
        FreeCAD.Console.PrintMessage("Context menu action triggered\n")

    def claimChildren(self):
        """Return list of child objects to show in tree hierarchy."""
        # return [self.Object.BaseFeature] if hasattr(self.Object, "BaseFeature") else []
        return []

    def __getstate__(self):
        return None

    def __setstate__(self, state):
        return None
```

## Creating the Object

```python
def makeMyObject(name="MyObject"):
    """Factory function to create the parametric object."""
    doc = FreeCAD.ActiveDocument
    if doc is None:
        doc = FreeCAD.newDocument()

    obj = doc.addObject("Part::FeaturePython", name)
    MyParametricObject(obj)

    if FreeCAD.GuiUp:
        ViewProviderMyObject(obj.ViewObject)

    doc.recompute()
    return obj

# Usage
obj = makeMyObject("ChamferedBlock")
obj.Length = 20.0
obj.Chamfered = True
FreeCAD.ActiveDocument.recompute()
```

## Complete Property Type Reference

### Numeric Properties

| Type | Python | Notes |
|---|---|---|
| `App::PropertyInteger` | `int` | Standard integer |
| `App::PropertyFloat` | `float` | Standard float |
| `App::PropertyLength` | `float` | Length with units (mm) |
| `App::PropertyDistance` | `float` | Distance (can be negative) |
| `App::PropertyAngle` | `float` | Angle in degrees |
| `App::PropertyArea` | `float` | Area with units |
| `App::PropertyVolume` | `float` | Volume with units |
| `App::PropertySpeed` | `float` | Speed with units |
| `App::PropertyAcceleration` | `float` | Acceleration |
| `App::PropertyForce` | `float` | Force |
| `App::PropertyPressure` | `float` | Pressure |
| `App::PropertyPercent` | `int` | 0-100 integer |
| `App::PropertyQuantity` | `Quantity` | Generic unit-aware value |
| `App::PropertyIntegerConstraint` | `(val,min,max,step)` | Bounded integer |
| `App::PropertyFloatConstraint` | `(val,min,max,step)` | Bounded float |

### String/Path Properties

| Type | Python | Notes |
|---|---|---|
| `App::PropertyString` | `str` | Text string |
| `App::PropertyFont` | `str` | Font name |
| `App::PropertyFile` | `str` | File path |
| `App::PropertyFileIncluded` | `str` | Embedded file |
| `App::PropertyPath` | `str` | Directory path |

### Boolean and Enumeration

| Type | Python | Notes |
|---|---|---|
| `App::PropertyBool` | `bool` | True/False |
| `App::PropertyEnumeration` | `list`/`str` | Dropdown; set list then value |

```python
# Enumeration usage
obj.addProperty("App::PropertyEnumeration", "Style", "Options", "Style choice")
obj.Style = ["Solid", "Wireframe", "Points"]  # set choices FIRST
obj.Style = "Solid"                              # then set value
```

### Geometric Properties

| Type | Python | Notes |
|---|---|---|
| `App::PropertyVector` | `FreeCAD.Vector` | 3D vector |
| `App::PropertyVectorList` | `[Vector,...]` | List of vectors |
| `App::PropertyPlacement` | `Placement` | Position + rotation |
| `App::PropertyMatrix` | `Matrix` | 4x4 matrix |
| `App::PropertyVectorDistance` | `Vector` | Vector with units |
| `App::PropertyPosition` | `Vector` | Position with units |
| `App::PropertyDirection` | `Vector` | Direction vector |

### Link Properties

| Type | Python | Notes |
|---|---|---|
| `App::PropertyLink` | obj ref | Link to one object |
| `App::PropertyLinkList` | `[obj,...]` | Link to multiple objects |
| `App::PropertyLinkSub` | `(obj, [subs])` | Link with sub-elements |
| `App::PropertyLinkSubList` | `[(obj,[subs]),...]` | Multiple link+subs |
| `App::PropertyLinkChild` | obj ref | Claimed child link |
| `App::PropertyLinkListChild` | `[obj,...]` | Multiple claimed children |

### Shape and Material

| Type | Python | Notes |
|---|---|---|
| `Part::PropertyPartShape` | `Part.Shape` | Full shape |
| `App::PropertyColor` | `(r,g,b)` | Color (0.0-1.0) |
| `App::PropertyColorList` | `[(r,g,b),...]` | Color per element |
| `App::PropertyMaterial` | `Material` | Material definition |

### Container Properties

| Type | Python | Notes |
|---|---|---|
| `App::PropertyPythonObject` | any | Serializable Python object |
| `App::PropertyIntegerList` | `[int,...]` | List of integers |
| `App::PropertyFloatList` | `[float,...]` | List of floats |
| `App::PropertyStringList` | `[str,...]` | List of strings |
| `App::PropertyBoolList` | `[bool,...]` | List of booleans |
| `App::PropertyMap` | `{str:str}` | String dictionary |

## Object Dependency Tracking

```python
# InList: objects that reference this object
obj.InList          # [objects referencing obj]
obj.InListRecursive # all ancestors

# OutList: objects this object references
obj.OutList         # [objects obj references]
obj.OutListRecursive # all descendants
```

## Migration Between Versions

```python
class MyParametricObject:
    # ... existing code ...

    def onDocumentRestored(self, obj):
        """Handle version migration when document loads."""
        # Add properties that didn't exist in older versions
        if not hasattr(obj, "NewProp"):
            obj.addProperty("App::PropertyFloat", "NewProp", "Group", "Tip")
            obj.NewProp = default_value

        # Rename properties (copy value, remove old)
        if hasattr(obj, "OldPropName"):
            if not hasattr(obj, "NewPropName"):
                obj.addProperty("App::PropertyFloat", "NewPropName", "Group", "Tip")
                obj.NewPropName = obj.OldPropName
            obj.removeProperty("OldPropName")
```

## Attachment Support

```python
import Part

class MyAttachableObject:
    def __init__(self, obj):
        obj.Proxy = self
        obj.addExtension("Part::AttachExtensionPython")

    def execute(self, obj):
        # The attachment sets the Placement automatically
        if not obj.MapPathParameter:
            obj.positionBySupport()
        # Build your shape at the origin; Placement handles positioning
        obj.Shape = Part.makeBox(10, 10, 10)
```
scripting-fundamentals.md 5.9 KB
# FreeCAD Scripting Fundamentals

Reference guide for FreeCAD Python scripting basics: the document model, the console, objects, selection, and the Python environment.

## Official Wiki References

- [A gentle introduction](https://wiki.freecad.org/Manual:A_gentle_introduction)
- [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)
- [Scripting and macros](https://wiki.freecad.org/Scripting_and_macros)
- [Working with macros](https://wiki.freecad.org/Macros)
- [Code snippets](https://wiki.freecad.org/Code_snippets)
- [Debugging](https://wiki.freecad.org/Debugging)
- [Profiling](https://wiki.freecad.org/Profiling)
- [Python development environment](https://wiki.freecad.org/Python_Development_Environment)
- [Extra python modules](https://wiki.freecad.org/Extra_python_modules)
- [FreeCAD vector math library](https://wiki.freecad.org/FreeCAD_vector_math_library)
- [Embedding FreeCAD](https://wiki.freecad.org/Embedding_FreeCAD)
- [Embedding FreeCADGui](https://wiki.freecad.org/Embedding_FreeCADGui)
- [Macro at startup](https://wiki.freecad.org/Macro_at_Startup)
- [How to install macros](https://wiki.freecad.org/How_to_install_macros)
- [IPython notebook integration](https://wiki.freecad.org/IPython_notebook_integration)
- [Quantity](https://wiki.freecad.org/Quantity)

## The FreeCAD Module Hierarchy

```
FreeCAD (App)          — Core application, documents, objects, properties
ā”œā”€ā”€ FreeCAD.Vector     — 3D vector
ā”œā”€ā”€ FreeCAD.Rotation   — Quaternion rotation
ā”œā”€ā”€ FreeCAD.Placement  — Position + rotation
ā”œā”€ā”€ FreeCAD.Matrix     — 4x4 transformation matrix
ā”œā”€ā”€ FreeCAD.Units      — Unit conversion and quantities
ā”œā”€ā”€ FreeCAD.Console    — Message output
└── FreeCAD.Base       — Base types

FreeCADGui (Gui)       — GUI module (only when GUI is active)
ā”œā”€ā”€ Selection          — Selection management
ā”œā”€ā”€ Control            — Task panel management
ā”œā”€ā”€ ActiveDocument     — GUI document wrapper
└── getMainWindow()    — Qt main window
```

## Document Operations

```python
import FreeCAD

# Document lifecycle
doc = FreeCAD.newDocument("DocName")
doc = FreeCAD.openDocument("/path/to/file.FCStd")
doc = FreeCAD.ActiveDocument
FreeCAD.setActiveDocument("DocName")
doc.save()
doc.saveAs("/path/to/newfile.FCStd")
FreeCAD.closeDocument("DocName")

# Object management
obj = doc.addObject("Part::Feature", "ObjectName")
obj = doc.addObject("Part::FeaturePython", "CustomObj")
obj = doc.addObject("App::DocumentObjectGroup", "Group")
doc.removeObject("ObjectName")

# Object access
obj = doc.getObject("ObjectName")
obj = doc.ObjectName                # attribute syntax
all_objs = doc.Objects              # all objects in document
names = doc.findObjects("Part::Feature")  # by type

# Recompute
doc.recompute()                     # recompute all
doc.recompute([obj1, obj2])         # recompute specific objects
obj.touch()                         # mark as needing recompute
```

## Selection API

```python
import FreeCADGui

# Get selection
sel = FreeCADGui.Selection.getSelection()          # [obj, ...]
sel = FreeCADGui.Selection.getSelection("DocName") # from specific doc
sel_ex = FreeCADGui.Selection.getSelectionEx()      # extended info

# Extended selection details
for s in sel_ex:
    print(s.Object.Name)           # parent object
    print(s.SubElementNames)       # ("Face1", "Edge3", ...)
    print(s.SubObjects)            # actual sub-shapes
    for pt in s.PickedPoints:
        print(pt)                  # 3D pick point

# Set selection
FreeCADGui.Selection.addSelection(obj)
FreeCADGui.Selection.addSelection(obj, "Face1")
FreeCADGui.Selection.removeSelection(obj)
FreeCADGui.Selection.clearSelection()

# Selection observer
class MySelectionObserver:
    def addSelection(self, doc, obj, sub, pos):
        print(f"Selected: {obj}.{sub} at {pos}")
    def removeSelection(self, doc, obj, sub):
        print(f"Deselected: {obj}.{sub}")
    def setSelection(self, doc):
        print(f"Selection set changed in {doc}")
    def clearSelection(self, doc):
        print(f"Selection cleared in {doc}")

obs = MySelectionObserver()
FreeCADGui.Selection.addObserver(obs)
# Later: FreeCADGui.Selection.removeObserver(obs)
```

## Console and Logging

```python
FreeCAD.Console.PrintMessage("Normal message\n")   # blue/default
FreeCAD.Console.PrintWarning("Warning\n")           # orange
FreeCAD.Console.PrintError("Error\n")               # red
FreeCAD.Console.PrintLog("Debug info\n")            # log only

# Console message observer
class MyLogger:
    def __init__(self):
        FreeCAD.Console.PrintMessage("Logger started\n")
    def receive(self, msg):
        # process msg
        pass
```

## Units and Quantities

```python
from FreeCAD import Units

# Create quantities
q = Units.Quantity("10 mm")
q = Units.Quantity("1 in")
q = Units.Quantity(25.4, Units.Unit("mm"))
q = Units.parseQuantity("3.14 rad")

# Convert
value_mm = float(q)                    # internal unit (mm for length)
value_in = q.getValueAs("in")          # convert to other unit
value_m = q.getValueAs("m")

# Available unit schemes: mm/kg/s (FreeCAD default), SI, Imperial, etc.
# Common units: mm, m, in, ft, deg, rad, kg, g, lb, s, min, hr
```

## Property System

```python
# Add properties to any DocumentObject
obj.addProperty("App::PropertyFloat", "MyProp", "GroupName", "Tooltip")
obj.MyProp = 42.0

# Check property existence
if hasattr(obj, "MyProp"):
    print(obj.MyProp)

# Property metadata
obj.getPropertyByName("MyProp")
obj.getTypeOfProperty("MyProp")        # returns list: ["App::PropertyFloat"]
obj.getDocumentationOfProperty("MyProp")
obj.getGroupOfProperty("MyProp")

# Set property as read-only, hidden, etc.
obj.setPropertyStatus("MyProp", "ReadOnly")
obj.setPropertyStatus("MyProp", "Hidden")
obj.setPropertyStatus("MyProp", "-ReadOnly")   # remove status
# Statuses: ReadOnly, Hidden, Transient, Output, NoRecompute
```
workbenches-and-advanced.md 9.8 KB
# FreeCAD Workbenches and Advanced Topics

Reference guide for workbench creation, macros, FEM scripting, Path/CAM scripting, and advanced recipes.

## Official Wiki References

- [Workbench creation](https://wiki.freecad.org/Workbench_creation)
- [Script tutorial](https://wiki.freecad.org/Scripts)
- [Macros recipes](https://wiki.freecad.org/Macros_recipes)
- [FEM scripting](https://wiki.freecad.org/FEM_Tutorial_Python)
- [Path scripting](https://wiki.freecad.org/Path_scripting)
- [Raytracing scripting](https://wiki.freecad.org/Raytracing_API_example)
- [Svg namespace](https://wiki.freecad.org/Svg_Namespace)
- [Python](https://wiki.freecad.org/Python)
- [PythonOCC](https://wiki.freecad.org/PythonOCC)

## Custom Workbench — Full Template

### Directory Structure

```
MyWorkbench/
ā”œā”€ā”€ __init__.py          # Empty or minimal
ā”œā”€ā”€ Init.py              # Runs at FreeCAD startup (no GUI)
ā”œā”€ā”€ InitGui.py           # Runs at GUI startup (defines workbench)
ā”œā”€ā”€ MyCommands.py        # Command implementations
ā”œā”€ā”€ Resources/
│   ā”œā”€ā”€ icons/
│   │   ā”œā”€ā”€ MyWorkbench.svg
│   │   └── MyCommand.svg
│   └── translations/    # Optional i18n
└── README.md
```

### Init.py

```python
# Runs at FreeCAD startup (before GUI)
# Register importers/exporters, add module paths, etc.
import FreeCAD
FreeCAD.addImportType("My Format (*.myf)", "MyImporter")
FreeCAD.addExportType("My Format (*.myf)", "MyExporter")
```

### InitGui.py

```python
import FreeCADGui

class MyWorkbench(FreeCADGui.Workbench):
    """Custom FreeCAD workbench."""

    MenuText = "My Workbench"
    ToolTip = "A custom workbench for specialized tasks"

    def __init__(self):
        import os
        self.__class__.Icon = os.path.join(
            os.path.dirname(__file__), "Resources", "icons", "MyWorkbench.svg"
        )

    def Initialize(self):
        """Called when workbench is first activated."""
        import MyCommands  # deferred import

        # Define toolbars
        self.appendToolbar("My Tools", [
            "My_CreateBox",
            "Separator",    # toolbar separator
            "My_EditObject"
        ])

        # Define menus
        self.appendMenu("My Workbench", [
            "My_CreateBox",
            "My_EditObject"
        ])

        # Submenus
        self.appendMenu(["My Workbench", "Advanced"], [
            "My_AdvancedCommand"
        ])

        import FreeCAD
        FreeCAD.Console.PrintMessage("My Workbench initialized\n")

    def Activated(self):
        """Called when workbench is switched to."""
        pass

    def Deactivated(self):
        """Called when leaving the workbench."""
        pass

    def ContextMenu(self, recipient):
        """Called for right-click context menus."""
        self.appendContextMenu("My Tools", ["My_CreateBox"])

    def GetClassName(self):
        return "Gui::PythonWorkbench"

FreeCADGui.addWorkbench(MyWorkbench)
```

### MyCommands.py

```python
import FreeCAD
import FreeCADGui
import os

ICON_PATH = os.path.join(os.path.dirname(__file__), "Resources", "icons")

class CmdCreateBox:
    def GetResources(self):
        return {
            "Pixmap": os.path.join(ICON_PATH, "MyCommand.svg"),
            "MenuText": "Create Box",
            "ToolTip": "Create a parametric box"
        }

    def IsActive(self):
        return FreeCAD.ActiveDocument is not None

    def Activated(self):
        import Part
        doc = FreeCAD.ActiveDocument
        box = Part.makeBox(10, 10, 10)
        Part.show(box, "MyBox")
        doc.recompute()

class CmdEditObject:
    def GetResources(self):
        return {
            "Pixmap": ":/icons/edit-undo.svg",
            "MenuText": "Edit Object",
            "ToolTip": "Edit selected object"
        }

    def IsActive(self):
        return len(FreeCADGui.Selection.getSelection()) > 0

    def Activated(self):
        sel = FreeCADGui.Selection.getSelection()[0]
        FreeCAD.Console.PrintMessage(f"Editing {sel.Name}\n")

# Register commands
FreeCADGui.addCommand("My_CreateBox", CmdCreateBox())
FreeCADGui.addCommand("My_EditObject", CmdEditObject())
```

### Installing a Workbench

Place the workbench folder in one of:

```python
# User macro folder
FreeCAD.getUserMacroDir(True)

# User mod folder (preferred)
os.path.join(FreeCAD.getUserAppDataDir(), "Mod")

# System mod folder
os.path.join(FreeCAD.getResourceDir(), "Mod")
```

## FEM Scripting

```python
import FreeCAD
import ObjectsFem
import Fem
import femmesh.femmesh2mesh

doc = FreeCAD.ActiveDocument

# Get the solid object to analyse (must already exist in the document)
obj = doc.getObject("Body") or doc.Objects[0]

# Create analysis
analysis = ObjectsFem.makeAnalysis(doc, "Analysis")

# Create a solver
solver = ObjectsFem.makeSolverCalculixCcxTools(doc, "Solver")
analysis.addObject(solver)

# Material
material = ObjectsFem.makeMaterialSolid(doc, "Steel")
mat = material.Material
mat["Name"] = "Steel"
mat["YoungsModulus"] = "210000 MPa"
mat["PoissonRatio"] = "0.3"
mat["Density"] = "7900 kg/m^3"
material.Material = mat
analysis.addObject(material)

# Fixed constraint
fixed = ObjectsFem.makeConstraintFixed(doc, "Fixed")
fixed.References = [(obj, "Face1")]
analysis.addObject(fixed)

# Force constraint
force = ObjectsFem.makeConstraintForce(doc, "Force")
force.References = [(obj, "Face6")]
force.Force = 1000.0  # Newtons
force.Direction = (obj, ["Edge1"])
force.Reversed = False
analysis.addObject(force)

# Mesh
mesh = ObjectsFem.makeMeshGmsh(doc, "FEMMesh")
mesh.Part = obj
mesh.CharacteristicLengthMax = 5.0
analysis.addObject(mesh)

doc.recompute()

# Run solver
from femtools import ccxtools
fea = ccxtools.FemToolsCcx(analysis, solver)
fea.update_objects()
fea.setup_working_dir()
fea.setup_ccx()
fea.write_inp_file()
fea.ccx_run()
fea.load_results()
```

## Path/CAM Scripting

```python
import Path
import FreeCAD

# Create a path
commands = []
commands.append(Path.Command("G0", {"X": 0, "Y": 0, "Z": 5}))   # Rapid move
commands.append(Path.Command("G1", {"X": 10, "Y": 0, "Z": 0, "F": 100}))  # Feed
commands.append(Path.Command("G1", {"X": 10, "Y": 10, "Z": 0}))
commands.append(Path.Command("G1", {"X": 0, "Y": 10, "Z": 0}))
commands.append(Path.Command("G1", {"X": 0, "Y": 0, "Z": 0}))
commands.append(Path.Command("G0", {"Z": 5}))   # Retract

path = Path.Path(commands)

# Add to document
doc = FreeCAD.ActiveDocument
path_obj = doc.addObject("Path::Feature", "MyPath")
path_obj.Path = path

# G-code output
gcode = path.toGCode()
print(gcode)
```

## Common Recipes

### Mirror a Shape

```python
import Part
import FreeCAD
shape = obj.Shape
mirrored = shape.mirror(FreeCAD.Vector(0,0,0), FreeCAD.Vector(1,0,0))  # mirror about YZ
Part.show(mirrored, "Mirrored")
```

### Array of Shapes

```python
import Part
import FreeCAD

def linear_array(shape, direction, count, spacing):
    """Create a linear array compound."""
    shapes = []
    for i in range(count):
        offset = FreeCAD.Vector(direction)
        offset.multiply(i * spacing)
        moved = shape.copy()
        moved.translate(offset)
        shapes.append(moved)
    return Part.Compound(shapes)

result = linear_array(obj.Shape, FreeCAD.Vector(1,0,0), 5, 15.0)
Part.show(result, "Array")
```

### Circular/Polar Array

```python
import Part
import FreeCAD
import math

def polar_array(shape, axis, center, count):
    """Create a polar array compound."""
    shapes = []
    angle = 360.0 / count
    for i in range(count):
        rot = FreeCAD.Rotation(axis, angle * i)
        placement = FreeCAD.Placement(FreeCAD.Vector(0,0,0), rot, center)
        moved = shape.copy()
        moved.Placement = placement
        shapes.append(moved)
    return Part.Compound(shapes)

result = polar_array(obj.Shape, FreeCAD.Vector(0,0,1), FreeCAD.Vector(0,0,0), 8)
Part.show(result, "PolarArray")
```

### Measure Distance Between Shapes

```python
dist = shape1.distToShape(shape2)
# Returns: (min_distance, [(point_on_shape1, point_on_shape2), ...], ...)
min_dist = dist[0]
closest_points = dist[1]  # List of (Vector, Vector) pairs
```

### Create a Tube/Pipe

```python
import Part

outer_cyl = Part.makeCylinder(outer_radius, height)
inner_cyl = Part.makeCylinder(inner_radius, height)
tube = outer_cyl.cut(inner_cyl)
Part.show(tube, "Tube")
```

### Assign Color to Faces

```python
# Set per-face colors
obj.ViewObject.DiffuseColor = [
    (1.0, 0.0, 0.0, 0.0),   # Face1 = red
    (0.0, 1.0, 0.0, 0.0),   # Face2 = green
    (0.0, 0.0, 1.0, 0.0),   # Face3 = blue
    # ... one tuple per face, (R, G, B, transparency)
]

# Or set single color for whole object
obj.ViewObject.ShapeColor = (0.8, 0.2, 0.2)
```

### Batch Export All Objects

```python
import FreeCAD
import Part
import os

doc = FreeCAD.ActiveDocument
export_dir = "/path/to/export"

if doc is None:
    FreeCAD.Console.PrintMessage("No active document to export.\n")
else:
    os.makedirs(export_dir, exist_ok=True)

    for obj in doc.Objects:
        if hasattr(obj, "Shape") and obj.Shape.Solids:
            filepath = os.path.join(export_dir, f"{obj.Name}.step")
            Part.export([obj], filepath)
            FreeCAD.Console.PrintMessage(f"Exported {filepath}\n")
```

### Timer / Progress Bar

```python
from PySide2 import QtWidgets, QtCore

# Simple progress dialog
progress = QtWidgets.QProgressDialog("Processing...", "Cancel", 0, total_steps)
progress.setWindowModality(QtCore.Qt.WindowModal)

for i in range(total_steps):
    if progress.wasCanceled():
        break
    # ... do work ...
    progress.setValue(i)

progress.setValue(total_steps)
```

### Run a Macro Programmatically

```python
import FreeCADGui
import runpy

# Execute a macro file
FreeCADGui.runCommand("Std_Macro")  # Opens macro dialog

# Only execute trusted macros. Prefer an explicit path and a clearer runner.
runpy.run_path("/path/to/macro.py", run_name="__main__")

# Or use the FreeCAD macro runner with the same trusted, explicit path
FreeCADGui.doCommand('import runpy; runpy.run_path("/path/to/macro.py", run_name="__main__")')
```

License (MIT)

View full license text
MIT License

Copyright GitHub, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.