From 5bfb933b270b665a7999f9fffa0694afea793e8c Mon Sep 17 00:00:00 2001 From: jhauga Date: Tue, 7 Apr 2026 22:13:14 -0400 Subject: [PATCH 1/6] new skill freecad-scripts --- docs/README.skills.md | 1 + skills/freecad-scripts/SKILL.md | 688 ++++++++++++++++++ .../references/geometry-and-shapes.md | 302 ++++++++ .../references/gui-and-interface.md | 383 ++++++++++ .../references/parametric-objects.md | 308 ++++++++ .../references/scripting-fundamentals.md | 176 +++++ .../references/workbenches-and-advanced.md | 387 ++++++++++ 7 files changed, 2245 insertions(+) create mode 100644 skills/freecad-scripts/SKILL.md create mode 100644 skills/freecad-scripts/references/geometry-and-shapes.md create mode 100644 skills/freecad-scripts/references/gui-and-interface.md create mode 100644 skills/freecad-scripts/references/parametric-objects.md create mode 100644 skills/freecad-scripts/references/scripting-fundamentals.md create mode 100644 skills/freecad-scripts/references/workbenches-and-advanced.md diff --git a/docs/README.skills.md b/docs/README.skills.md index 4af14033d..a5d602c44 100644 --- a/docs/README.skills.md +++ b/docs/README.skills.md @@ -119,6 +119,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-skills) for guidelines on how to | [flowstudio-power-automate-mcp](../skills/flowstudio-power-automate-mcp/SKILL.md) | Connect to and operate Power Automate cloud flows via a FlowStudio MCP server. Use when asked to: list flows, read a flow definition, check run history, inspect action outputs, resubmit a run, cancel a running flow, view connections, get a trigger URL, validate a definition, monitor flow health, or any task that requires talking to the Power Automate API through an MCP tool. Also use for Power Platform environment discovery and connection management. Requires a FlowStudio MCP subscription or compatible server — see https://mcp.flowstudio.app | `references/MCP-BOOTSTRAP.md`
`references/action-types.md`
`references/connection-references.md`
`references/tool-reference.md` | | [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` | | [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` | | [gen-specs-as-issues](../skills/gen-specs-as-issues/SKILL.md) | This workflow guides you through a systematic approach to identify missing features, prioritize them, and create detailed specifications for implementation. | None | | [generate-custom-instructions-from-codebase](../skills/generate-custom-instructions-from-codebase/SKILL.md) | Migration and code evolution instructions generator for GitHub Copilot. Analyzes differences between two project versions (branches, commits, or releases) to create precise instructions allowing Copilot to maintain consistency during technology migrations, major refactoring, or framework version upgrades. | None | diff --git a/skills/freecad-scripts/SKILL.md b/skills/freecad-scripts/SKILL.md new file mode 100644 index 000000000..b5519a1b7 --- /dev/null +++ b/skills/freecad-scripts/SKILL.md @@ -0,0 +1,688 @@ +--- +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) +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 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: +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 MeshPart +mesh = MeshPart.meshFromShape(Shape=obj.Shape, LinearDeflection=0.1, + AngularDeflection=0.5) + +# Convert Mesh to Part shape +import Part +shape = Part.Shape() +shape.makeShapeFromMesh(mesh.Topology, 0.05) # tolerance +solid = Part.makeSolid(shape) +``` + +### Sketcher Module + +```python +import Sketcher +import Part + +# 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 FreeCADGui +import Part +from FreeCAD import Base + +# Guard for GUI availability +if FreeCAD.GuiUp: + from PySide2 import QtWidgets, QtCore + +def main(): + doc = FreeCAD.ActiveDocument + if doc is None: + FreeCAD.Console.PrintError("No active document\n") + return + + 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..690913874 --- /dev/null +++ b/skills/freecad-scripts/references/geometry-and-shapes.md @@ -0,0 +1,302 @@ +# 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 +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..46abef17a --- /dev/null +++ b/skills/freecad-scripts/references/gui-and-interface.md @@ -0,0 +1,383 @@ +# 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 +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 +from PySide2 import QtWidgets, QtCore, QtGui + +class MyDialog(QtWidgets.QDialog): + def __init__(self, parent=None): + super().__init__(parent or FreeCADGui.getMainWindow()) + 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 +from PySide2 import QtWidgets, QtUiTools + +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..eb770847d --- /dev/null +++ b/skills/freecad-scripts/references/workbenches-and-advanced.md @@ -0,0 +1,387 @@ +# 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. +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" + ]) + + 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 + +# 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 +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 Part +import os + +doc = FreeCAD.ActiveDocument +export_dir = "/path/to/export" +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 +# Execute a macro file +FreeCADGui.runCommand("Std_Macro") # Opens macro dialog + +# Or run directly +exec(open("/path/to/macro.py").read()) + +# Or use the FreeCAD macro runner +FreeCADGui.doCommand('exec(open("/path/to/macro.py").read())') +``` From 5642b9b5c16f1c1aa995d3fc78df3dff4c7140e3 Mon Sep 17 00:00:00 2001 From: John Haugabook Date: Tue, 7 Apr 2026 22:38:17 -0400 Subject: [PATCH 2/6] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- skills/freecad-scripts/SKILL.md | 9 +++------ skills/freecad-scripts/references/gui-and-interface.md | 4 +++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/skills/freecad-scripts/SKILL.md b/skills/freecad-scripts/SKILL.md index b5519a1b7..06333d757 100644 --- a/skills/freecad-scripts/SKILL.md +++ b/skills/freecad-scripts/SKILL.md @@ -214,12 +214,13 @@ mesh = Mesh.Mesh("/path/to/file.stl") mesh.write("/path/to/output.stl") # Convert Part shape to Mesh +import Part import MeshPart -mesh = MeshPart.meshFromShape(Shape=obj.Shape, LinearDeflection=0.1, +shape = Part.makeBox(1, 1, 1) +mesh = MeshPart.meshFromShape(Shape=shape, LinearDeflection=0.1, AngularDeflection=0.5) # Convert Mesh to Part shape -import Part shape = Part.Shape() shape.makeShapeFromMesh(mesh.Topology, 0.05) # tolerance solid = Part.makeSolid(shape) @@ -227,10 +228,6 @@ solid = Part.makeSolid(shape) ### Sketcher Module -```python -import Sketcher -import Part - # Create a sketch on XY plane sketch = doc.addObject("Sketcher::SketchObject", "MySketch") sketch.Placement = FreeCAD.Placement( diff --git a/skills/freecad-scripts/references/gui-and-interface.md b/skills/freecad-scripts/references/gui-and-interface.md index 46abef17a..6645e25e2 100644 --- a/skills/freecad-scripts/references/gui-and-interface.md +++ b/skills/freecad-scripts/references/gui-and-interface.md @@ -170,11 +170,13 @@ class MultiFormPanel: ## 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()) + super().__init__(parent or (FreeCADGui.getMainWindow() if FreeCAD.GuiUp else None)) self.setWindowTitle("My Dialog") self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint) From 75b7f3debc475f826a52cb6646fe5a92eef800e0 Mon Sep 17 00:00:00 2001 From: jhauga Date: Tue, 7 Apr 2026 22:42:12 -0400 Subject: [PATCH 3/6] Apply suggestions from code review --- skills/freecad-scripts/SKILL.md | 2 ++ skills/freecad-scripts/references/geometry-and-shapes.md | 2 ++ skills/freecad-scripts/references/gui-and-interface.md | 5 ++++- .../freecad-scripts/references/workbenches-and-advanced.md | 5 +++++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/skills/freecad-scripts/SKILL.md b/skills/freecad-scripts/SKILL.md index 06333d757..d8234e3d5 100644 --- a/skills/freecad-scripts/SKILL.md +++ b/skills/freecad-scripts/SKILL.md @@ -99,6 +99,7 @@ placement = FreeCAD.Placement( obj.Placement = placement # Matrix (4x4 transformation) +import math mat = FreeCAD.Matrix() mat.move(FreeCAD.Vector(10, 0, 0)) mat.rotateZ(math.radians(45)) @@ -109,6 +110,7 @@ mat.rotateZ(math.radians(45)) The Part module wraps OpenCASCADE and provides BRep solid modeling: ```python +import FreeCAD import Part # --- Primitive Shapes --- diff --git a/skills/freecad-scripts/references/geometry-and-shapes.md b/skills/freecad-scripts/references/geometry-and-shapes.md index 690913874..6f6dd7a6d 100644 --- a/skills/freecad-scripts/references/geometry-and-shapes.md +++ b/skills/freecad-scripts/references/geometry-and-shapes.md @@ -296,6 +296,8 @@ mesh = MeshPart.meshFromShape( ) # 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 index 6645e25e2..b2d463bd8 100644 --- a/skills/freecad-scripts/references/gui-and-interface.md +++ b/skills/freecad-scripts/references/gui-and-interface.md @@ -63,6 +63,8 @@ FreeCADGui.addCommand("My_Command", MyCommand()) 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: @@ -209,7 +211,8 @@ class MyDialog(QtWidgets.QDialog): ### Loading a .ui File ```python -from PySide2 import QtWidgets, QtUiTools +import os +from PySide2 import QtWidgets, QtUiTools, QtCore def loadUiFile(ui_path): """Load a Qt Designer .ui file.""" diff --git a/skills/freecad-scripts/references/workbenches-and-advanced.md b/skills/freecad-scripts/references/workbenches-and-advanced.md index eb770847d..294c17efe 100644 --- a/skills/freecad-scripts/references/workbenches-and-advanced.md +++ b/skills/freecad-scripts/references/workbenches-and-advanced.md @@ -37,6 +37,7 @@ MyWorkbench/ ```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") ``` @@ -80,6 +81,7 @@ class MyWorkbench(FreeCADGui.Workbench): "My_AdvancedCommand" ]) + import FreeCAD FreeCAD.Console.PrintMessage("My Workbench initialized\n") def Activated(self): @@ -172,6 +174,9 @@ 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") From 613a21b654398d820f2e8f748507815eecc9288e Mon Sep 17 00:00:00 2001 From: jhauga Date: Tue, 7 Apr 2026 22:55:09 -0400 Subject: [PATCH 4/6] resolve: codepsellrc, readme --- .codespellrc | 4 +++- docs/README.skills.md | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.codespellrc b/.codespellrc index b9282db12..e36eff4c5 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 6d4833e87..beb42ff8a 100644 --- a/docs/README.skills.md +++ b/docs/README.skills.md @@ -142,7 +142,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-skills) for guidelines on how to | [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, or at the intersection of analytical and intuitive. Not shown directly to users — informs how Ember shows up. | None | +| [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` | | [gen-specs-as-issues](../skills/gen-specs-as-issues/SKILL.md) | This workflow guides you through a systematic approach to identify missing features, prioritize them, and create detailed specifications for implementation. | None | From e96cd1456ff9e36b2805bd13dc30762dff8b93b7 Mon Sep 17 00:00:00 2001 From: John Haugabook Date: Tue, 7 Apr 2026 23:02:01 -0400 Subject: [PATCH 5/6] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .codespellrc | 2 +- skills/freecad-scripts/SKILL.md | 1 + .../references/workbenches-and-advanced.md | 29 ++++++++++++------- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/.codespellrc b/.codespellrc index e36eff4c5..adc8610c8 100644 --- a/.codespellrc +++ b/.codespellrc @@ -46,7 +46,7 @@ # queston - intentional misspelling example in skills/arize-dataset/SKILL.md demonstrating typo detection in field names -# Vertexes - freeCAD shape sub-elements used as property of obj.Shape +# 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 diff --git a/skills/freecad-scripts/SKILL.md b/skills/freecad-scripts/SKILL.md index d8234e3d5..3cf524efb 100644 --- a/skills/freecad-scripts/SKILL.md +++ b/skills/freecad-scripts/SKILL.md @@ -164,6 +164,7 @@ 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() diff --git a/skills/freecad-scripts/references/workbenches-and-advanced.md b/skills/freecad-scripts/references/workbenches-and-advanced.md index 294c17efe..0bad6f3fe 100644 --- a/skills/freecad-scripts/references/workbenches-and-advanced.md +++ b/skills/freecad-scripts/references/workbenches-and-advanced.md @@ -259,6 +259,7 @@ print(gcode) ```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") @@ -346,18 +347,23 @@ 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" -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") +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 @@ -381,12 +387,15 @@ progress.setValue(total_steps) ### Run a Macro Programmatically ```python +import FreeCADGui +import runpy + # Execute a macro file FreeCADGui.runCommand("Std_Macro") # Opens macro dialog -# Or run directly -exec(open("/path/to/macro.py").read()) +# 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 -FreeCADGui.doCommand('exec(open("/path/to/macro.py").read())') +# 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__")') ``` From 83e46712faca7e1af435a80f24ffda8e7ca0f6e0 Mon Sep 17 00:00:00 2001 From: jhauga Date: Tue, 7 Apr 2026 23:05:25 -0400 Subject: [PATCH 6/6] add suggestions from review --- skills/freecad-scripts/SKILL.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/skills/freecad-scripts/SKILL.md b/skills/freecad-scripts/SKILL.md index 3cf524efb..dda9f63fd 100644 --- a/skills/freecad-scripts/SKILL.md +++ b/skills/freecad-scripts/SKILL.md @@ -538,12 +538,12 @@ FreeCADGui.addWorkbench(MyWorkbench) # Date: 2026-04-07 import FreeCAD -import FreeCADGui import Part from FreeCAD import Base # Guard for GUI availability if FreeCAD.GuiUp: + import FreeCADGui from PySide2 import QtWidgets, QtCore def main(): @@ -552,9 +552,10 @@ def main(): FreeCAD.Console.PrintError("No active document\n") return - sel = FreeCADGui.Selection.getSelection() - if not sel: - FreeCAD.Console.PrintWarning("No objects selected\n") + if FreeCAD.GuiUp: + sel = FreeCADGui.Selection.getSelection() + if not sel: + FreeCAD.Console.PrintWarning("No objects selected\n") # ... macro logic ...