mirror of
https://github.com/github/awesome-copilot.git
synced 2026-04-12 19:25:55 +00:00
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:
689
skills/freecad-scripts/SKILL.md
Normal file
689
skills/freecad-scripts/SKILL.md
Normal 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
|
||||
304
skills/freecad-scripts/references/geometry-and-shapes.md
Normal file
304
skills/freecad-scripts/references/geometry-and-shapes.md
Normal 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)
|
||||
```
|
||||
388
skills/freecad-scripts/references/gui-and-interface.md
Normal file
388
skills/freecad-scripts/references/gui-and-interface.md
Normal 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()
|
||||
```
|
||||
308
skills/freecad-scripts/references/parametric-objects.md
Normal file
308
skills/freecad-scripts/references/parametric-objects.md
Normal 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)
|
||||
```
|
||||
176
skills/freecad-scripts/references/scripting-fundamentals.md
Normal file
176
skills/freecad-scripts/references/scripting-fundamentals.md
Normal 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
|
||||
```
|
||||
401
skills/freecad-scripts/references/workbenches-and-advanced.md
Normal file
401
skills/freecad-scripts/references/workbenches-and-advanced.md
Normal 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__")')
|
||||
```
|
||||
Reference in New Issue
Block a user