diff --git a/.codespellrc b/.codespellrc
index b9282db12..adc8610c8 100644
--- a/.codespellrc
+++ b/.codespellrc
@@ -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
diff --git a/docs/README.skills.md b/docs/README.skills.md
index 1e539338c..90d599174 100644
--- a/docs/README.skills.md
+++ b/docs/README.skills.md
@@ -143,6 +143,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`
`references/LAYOUT-AND-NAVIGATION.md`
`references/SETUP.md`
`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`
`references/gui-and-interface.md`
`references/parametric-objects.md`
`references/scripting-fundamentals.md`
`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`
`assets/2d-platform-game.md`
`assets/gameBase-template-repo.md`
`assets/paddle-game-template.md`
`assets/simple-2d-engine.md`
`references/3d-web-games.md`
`references/algorithms.md`
`references/basics.md`
`references/game-control-mechanisms.md`
`references/game-engine-core-principles.md`
`references/game-publishing.md`
`references/techniques.md`
`references/terminology.md`
`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`
`references/data-rights.md` |
diff --git a/skills/freecad-scripts/SKILL.md b/skills/freecad-scripts/SKILL.md
new file mode 100644
index 000000000..dda9f63fd
--- /dev/null
+++ b/skills/freecad-scripts/SKILL.md
@@ -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
diff --git a/skills/freecad-scripts/references/geometry-and-shapes.md b/skills/freecad-scripts/references/geometry-and-shapes.md
new file mode 100644
index 000000000..6f6dd7a6d
--- /dev/null
+++ b/skills/freecad-scripts/references/geometry-and-shapes.md
@@ -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)
+```
diff --git a/skills/freecad-scripts/references/gui-and-interface.md b/skills/freecad-scripts/references/gui-and-interface.md
new file mode 100644
index 000000000..b2d463bd8
--- /dev/null
+++ b/skills/freecad-scripts/references/gui-and-interface.md
@@ -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()
+```
diff --git a/skills/freecad-scripts/references/parametric-objects.md b/skills/freecad-scripts/references/parametric-objects.md
new file mode 100644
index 000000000..d56d644c1
--- /dev/null
+++ b/skills/freecad-scripts/references/parametric-objects.md
@@ -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)
+```
diff --git a/skills/freecad-scripts/references/scripting-fundamentals.md b/skills/freecad-scripts/references/scripting-fundamentals.md
new file mode 100644
index 000000000..336dc7a36
--- /dev/null
+++ b/skills/freecad-scripts/references/scripting-fundamentals.md
@@ -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
+```
diff --git a/skills/freecad-scripts/references/workbenches-and-advanced.md b/skills/freecad-scripts/references/workbenches-and-advanced.md
new file mode 100644
index 000000000..0bad6f3fe
--- /dev/null
+++ b/skills/freecad-scripts/references/workbenches-and-advanced.md
@@ -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__")')
+```