mirror of
https://github.com/github/awesome-copilot.git
synced 2026-04-12 19:25:55 +00:00
* 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>
402 lines
9.8 KiB
Markdown
402 lines
9.8 KiB
Markdown
# 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__")')
|
|
```
|