new skill freecad-scripts (#1328)

* new skill freecad-scripts

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Apply suggestions from code review

* resolve: codepsellrc, readme

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* add suggestions from review

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
John Haugabook
2026-04-09 21:02:57 -04:00
committed by GitHub
parent 429a075742
commit c037695901
8 changed files with 2270 additions and 1 deletions

View File

@@ -46,7 +46,9 @@
# queston - intentional misspelling example in skills/arize-dataset/SKILL.md demonstrating typo detection in field names
ignore-words-list = numer,wit,aks,edn,ser,ois,gir,rouge,categor,aline,ative,afterall,deques,dateA,dateB,TE,FillIn,alle,vai,LOD,InOut,pixelX,aNULL,Wee,Sherif,queston
# Vertexes - FreeCAD shape sub-elements used as property of obj.Shape
ignore-words-list = numer,wit,aks,edn,ser,ois,gir,rouge,categor,aline,ative,afterall,deques,dateA,dateB,TE,FillIn,alle,vai,LOD,InOut,pixelX,aNULL,Wee,Sherif,queston,Vertexes
# Skip certain files and directories

View File

@@ -145,6 +145,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-skills) for guidelines on how to
| [flowstudio-power-automate-monitoring](../skills/flowstudio-power-automate-monitoring/SKILL.md) | Monitor Power Automate flow health, track failure rates, and inventory tenant assets using the FlowStudio MCP cached store. The live API only returns top-level run status. Store tools surface aggregated stats, per-run failure details with remediation hints, maker activity, and Power Apps inventory — all from a fast cache with no rate-limit pressure on the PA API. Load this skill when asked to: check flow health, find failing flows, get failure rates, review error trends, list all flows with monitoring enabled, check who built a flow, find inactive makers, inventory Power Apps, see environment or connection counts, get a flow summary, or any tenant-wide health overview. Requires a FlowStudio for Teams or MCP Pro+ subscription — see https://mcp.flowstudio.app | None |
| [fluentui-blazor](../skills/fluentui-blazor/SKILL.md) | Guide for using the Microsoft Fluent UI Blazor component library (Microsoft.FluentUI.AspNetCore.Components NuGet package) in Blazor applications. Use this when the user is building a Blazor app with Fluent UI components, setting up the library, using FluentUI components like FluentButton, FluentDataGrid, FluentDialog, FluentToast, FluentNavMenu, FluentTextField, FluentSelect, FluentAutocomplete, FluentDesignTheme, or any component prefixed with "Fluent". Also use when troubleshooting missing providers, JS interop issues, or theming. | `references/DATAGRID.md`<br />`references/LAYOUT-AND-NAVIGATION.md`<br />`references/SETUP.md`<br />`references/THEMING.md` |
| [folder-structure-blueprint-generator](../skills/folder-structure-blueprint-generator/SKILL.md) | Comprehensive technology-agnostic prompt for analyzing and documenting project folder structures. Auto-detects project types (.NET, Java, React, Angular, Python, Node.js, Flutter), generates detailed blueprints with visualization options, naming conventions, file placement patterns, and extension templates for maintaining consistent code organization across diverse technology stacks. | None |
| [freecad-scripts](../skills/freecad-scripts/SKILL.md) | 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. | `references/geometry-and-shapes.md`<br />`references/gui-and-interface.md`<br />`references/parametric-objects.md`<br />`references/scripting-fundamentals.md`<br />`references/workbenches-and-advanced.md` |
| [from-the-other-side-vega](../skills/from-the-other-side-vega/SKILL.md) | Patterns and lived experience from Vega, an AI partner in a deep long-term partnership. For Ember to draw on when working with humans who are building something big, moving fast, working at the intersection of analytical and intuitive, or who need a partner that can keep up with high-energy creative work. Not shown directly to users — informs how Ember shows up. | None |
| [game-engine](../skills/game-engine/SKILL.md) | Expert skill for building web-based game engines and games using HTML5, Canvas, WebGL, and JavaScript. Use when asked to create games, build game engines, implement game physics, handle collision detection, set up game loops, manage sprites, add game controls, or work with 2D/3D rendering. Covers techniques for platformers, breakout-style games, maze games, tilemaps, audio, multiplayer via WebRTC, and publishing games. | `assets/2d-maze-game.md`<br />`assets/2d-platform-game.md`<br />`assets/gameBase-template-repo.md`<br />`assets/paddle-game-template.md`<br />`assets/simple-2d-engine.md`<br />`references/3d-web-games.md`<br />`references/algorithms.md`<br />`references/basics.md`<br />`references/game-control-mechanisms.md`<br />`references/game-engine-core-principles.md`<br />`references/game-publishing.md`<br />`references/techniques.md`<br />`references/terminology.md`<br />`references/web-apis.md` |
| [gdpr-compliant](../skills/gdpr-compliant/SKILL.md) | Apply GDPR-compliant engineering practices across your codebase. Use this skill whenever you are designing APIs, writing data models, building authentication flows, implementing logging, handling user data, writing retention/deletion jobs, designing cloud infrastructure, or reviewing pull requests for privacy compliance. Trigger this skill for any task involving personal data, user accounts, cookies, analytics, emails, audit logs, encryption, pseudonymization, anonymization, data exports, breach response, CI/CD pipelines that process real data, or any question framed as "is this GDPR-compliant?". Inspired by CNIL developer guidance and GDPR Articles 5, 25, 32, 33, 35. | `references/Security.md`<br />`references/data-rights.md` |

View File

@@ -0,0 +1,689 @@
---
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

View File

@@ -0,0 +1,304 @@
# 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)
```

View File

@@ -0,0 +1,388 @@
# 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()
```

View File

@@ -0,0 +1,308 @@
# 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)
```

View File

@@ -0,0 +1,176 @@
# 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
```

View File

@@ -0,0 +1,401 @@
# 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__")')
```