← Skills

FreeCAD Direct Modeling

Native Read-only

Part workbench primitives, booleans, transformations, and extrusions. Default skill injected at every stage that produces FreeCAD Python code.

/skills/direct_modeling.md

Estimated tokens
2361
Characters
9441
Source
Native

Markdown

# FreeCAD Direct Modeling Skill

This skill defines how to generate reliable FreeCAD code using **direct modeling** with the Part workbench.

Direct modeling is simpler, more reliable, and better suited for AI-generated CAD code.

## Why Direct Modeling?

| Aspect | Direct (Part) | Parametric (PartDesign) |
|--------|---------------|-------------------------|
| **Reliability** | High - no face references | Low - face refs are unstable |
| **Code complexity** | Simple | Complex (sketches, constraints) |
| **Error rate** | Low | High |
| **Editable in FreeCAD** | No (final shape only) | Yes (feature tree) |
| **Best for** | AI generation | Manual CAD work |

---

## Standard Code Structure

```python
import FreeCAD
import Part
import math
from FreeCAD import Vector, Placement, Rotation

# === PARAMETERS ===
LENGTH = 100
WIDTH = 60
HEIGHT = 40
HOLE_DIAMETER = 10

# === DOCUMENT ===
doc = FreeCAD.ActiveDocument or FreeCAD.newDocument("Model")

# === BUILD GEOMETRY ===
# Start with a base shape
base = Part.makeBox(LENGTH, WIDTH, HEIGHT)

# Add features using boolean operations
hole = Part.makeCylinder(HOLE_DIAMETER / 2, HEIGHT, Vector(LENGTH/2, WIDTH/2, 0))
result = base.cut(hole)

# === ADD TO DOCUMENT ===
feature = doc.addObject("Part::Feature", "Model")
feature.Shape = result

doc.recompute()

if FreeCAD.GuiUp:
    FreeCAD.Gui.ActiveDocument.ActiveView.fitAll()
```

---

## Primitive Shapes

### Box
```python
# Part.makeBox(length, width, height, position, direction)
box = Part.makeBox(100, 60, 40)

# Centered at origin
box = Part.makeBox(100, 60, 40, Vector(-50, -30, 0))

# With custom placement
box = Part.makeBox(100, 60, 40)
box.translate(Vector(10, 20, 30))
```

### Cylinder
```python
# Part.makeCylinder(radius, height, position, direction)
cyl = Part.makeCylinder(25, 50)  # At origin, along Z

# Horizontal cylinder (along X)
cyl = Part.makeCylinder(25, 100, Vector(0, 0, 0), Vector(1, 0, 0))

# Positioned
cyl = Part.makeCylinder(10, 30, Vector(50, 30, 0), Vector(0, 0, 1))
```

### Sphere
```python
# Part.makeSphere(radius, center)
sphere = Part.makeSphere(25)
sphere = Part.makeSphere(25, Vector(50, 50, 50))
```

### Cone
```python
# Part.makeCone(radius1, radius2, height, position, direction)
cone = Part.makeCone(30, 10, 50)  # Tapers from r=30 to r=10
cone = Part.makeCone(20, 0, 40)   # Pointed cone
```

### Torus
```python
# Part.makeTorus(majorRadius, minorRadius)
torus = Part.makeTorus(50, 10)  # Donut shape
```

---

## Boolean Operations

### Fusion (Union/Add)
```python
# Combine two shapes
box = Part.makeBox(100, 100, 50)
cyl = Part.makeCylinder(30, 80, Vector(50, 50, 0))

result = box.fuse(cyl)

# Multiple shapes
shape1.fuse([shape2, shape3, shape4])
```

### Cut (Subtract)
```python
# Remove material
base = Part.makeBox(100, 100, 50)
hole = Part.makeCylinder(15, 50, Vector(50, 50, 0))

result = base.cut(hole)

# Multiple cuts
base.cut([hole1, hole2, hole3])
```

### Common (Intersection)
```python
# Keep only overlapping volume
box = Part.makeBox(100, 100, 100)
sphere = Part.makeSphere(60, Vector(50, 50, 50))

result = box.common(sphere)  # Rounded box corner
```

---

## Transformations

### Translate (Move)
```python
shape = Part.makeBox(50, 50, 50)
shape.translate(Vector(100, 0, 0))  # Move 100mm along X
```

### Rotate
```python
import math

shape = Part.makeBox(50, 50, 50)

# Rotate around Z axis at origin
shape.rotate(Vector(0, 0, 0), Vector(0, 0, 1), 45)  # 45 degrees

# Rotate around center
center = Vector(25, 25, 25)
shape.rotate(center, Vector(0, 0, 1), 90)
```

### Mirror
```python
# Mirror across XY plane at Z=0
shape = Part.makeBox(50, 50, 50, Vector(10, 10, 10))
mirrored = shape.mirror(Vector(0, 0, 0), Vector(0, 0, 1))
```

### Scale
```python
# Scale uniformly
import FreeCAD
matrix = FreeCAD.Matrix()
matrix.scale(2, 2, 2)  # Double size
scaled = shape.transformGeometry(matrix)

# Non-uniform scale
matrix.scale(2, 1, 0.5)  # Stretch X, compress Z
```

---

## Complex Shapes

### Extrusion
```python
# Create a wire (closed profile)
wire = Part.makePolygon([
    Vector(0, 0, 0),
    Vector(100, 0, 0),
    Vector(100, 50, 0),
    Vector(50, 50, 0),
    Vector(50, 30, 0),
    Vector(0, 30, 0),
    Vector(0, 0, 0)  # Close the profile
])

face = Part.Face(wire)
solid = face.extrude(Vector(0, 0, 40))  # Extrude 40mm in Z
```

### Revolution
```python
# Profile to revolve (in XZ plane)
profile = Part.makePolygon([
    Vector(10, 0, 0),
    Vector(30, 0, 0),
    Vector(30, 0, 50),
    Vector(20, 0, 60),
    Vector(10, 0, 50),
    Vector(10, 0, 0)
])
face = Part.Face(Part.Wire(profile))

# Revolve around Z axis
solid = face.revolve(Vector(0, 0, 0), Vector(0, 0, 1), 360)
```

### Loft
```python
# Multiple profiles at different heights
def make_circle_wire(center, radius):
    circle = Part.Circle(center, Vector(0, 0, 1), radius)
    return Part.Wire(circle.toShape())

profiles = [
    make_circle_wire(Vector(0, 0, 0), 30),
    make_circle_wire(Vector(0, 0, 50), 40),
    make_circle_wire(Vector(0, 0, 100), 20),
]

solid = Part.makeLoft(profiles, True)  # True = solid
```

### Sweep
```python
# Sweep a profile along a path
profile = Part.makeCircle(10)
profile = Part.Wire(profile)

path = Part.makeLine(Vector(0, 0, 0), Vector(100, 50, 30))
path = Part.Wire(path)

solid = Part.makePipe(path, profile)
```

---

## Fillets and Chamfers

### Fillet (Round edges)
```python
box = Part.makeBox(100, 60, 40)

# Fillet specific edges by index
filleted = box.makeFillet(5, [box.Edges[0], box.Edges[4], box.Edges[8]])

# Fillet all edges
filleted = box.makeFillet(3, box.Edges)
```

### Chamfer
```python
box = Part.makeBox(100, 60, 40)
chamfered = box.makeChamfer(3, box.Edges)
```

---

## Shell (Hollow out)

```python
box = Part.makeBox(100, 60, 40)

# Shell with 2mm wall thickness, removing top face
# Faces are indexed; find top face by checking normals
top_face = None
for face in box.Faces:
    if face.normalAt(0, 0).z > 0.9:
        top_face = face
        break

shelled = box.makeShell([top_face], 2)  # 2mm walls
```

---

## Pattern (Arrays)

### Linear Array
```python
base = Part.makeCylinder(5, 10)
shapes = [base]

for i in range(1, 5):
    copy = base.copy()
    copy.translate(Vector(i * 20, 0, 0))
    shapes.append(copy)

result = shapes[0].fuse(shapes[1:])
```

### Polar Array
```python
import math

base = Part.makeCylinder(5, 10, Vector(30, 0, 0))
shapes = [base]

for i in range(1, 8):
    angle = i * 360 / 8
    copy = base.copy()
    copy.rotate(Vector(0, 0, 0), Vector(0, 0, 1), angle)
    shapes.append(copy)

result = shapes[0].fuse(shapes[1:])
```

---

## Complete Examples

### Bracket with Holes
```python
import FreeCAD
import Part
from FreeCAD import Vector

# Parameters
BRACKET_L = 80
BRACKET_W = 40
BRACKET_H = 5
HOLE_DIA = 8
HOLE_SPACING = 30

doc = FreeCAD.ActiveDocument or FreeCAD.newDocument("Bracket")

# Base plate
plate = Part.makeBox(BRACKET_L, BRACKET_W, BRACKET_H)

# Mounting holes
holes = []
for x in [BRACKET_L/2 - HOLE_SPACING/2, BRACKET_L/2 + HOLE_SPACING/2]:
    hole = Part.makeCylinder(HOLE_DIA/2, BRACKET_H, Vector(x, BRACKET_W/2, 0))
    holes.append(hole)

# Cut holes
result = plate.cut(holes)

# Add to document
feature = doc.addObject("Part::Feature", "Bracket")
feature.Shape = result
doc.recompute()
```

### Cup/Container
```python
import FreeCAD
import Part
from FreeCAD import Vector

# Parameters
OUTER_DIA = 80
INNER_DIA = 74
HEIGHT = 100
WALL_THICK = 3

doc = FreeCAD.ActiveDocument or FreeCAD.newDocument("Cup")

# Outer cylinder
outer = Part.makeCylinder(OUTER_DIA/2, HEIGHT)

# Inner cavity (leave bottom thickness)
inner = Part.makeCylinder(INNER_DIA/2, HEIGHT - WALL_THICK, Vector(0, 0, WALL_THICK))

# Cut to create hollow
result = outer.cut(inner)

feature = doc.addObject("Part::Feature", "Cup")
feature.Shape = result
doc.recompute()
```

### Gear (Simplified)
```python
import FreeCAD
import Part
import math
from FreeCAD import Vector

# Parameters
NUM_TEETH = 20
MODULE = 2
THICKNESS = 10
BORE_DIA = 10

pitch_dia = NUM_TEETH * MODULE
outer_dia = pitch_dia + 2 * MODULE
root_dia = pitch_dia - 2.5 * MODULE

doc = FreeCAD.ActiveDocument or FreeCAD.newDocument("Gear")

# Base cylinder
gear = Part.makeCylinder(outer_dia/2, THICKNESS)

# Cut tooth gaps
tooth_angle = 360 / NUM_TEETH
gap_angle = tooth_angle * 0.4

for i in range(NUM_TEETH):
    angle = i * tooth_angle

    # Create wedge-shaped gap
    gap = Part.makeBox(outer_dia, outer_dia/4, THICKNESS)
    gap.translate(Vector(root_dia/2, -outer_dia/8, 0))
    gap.rotate(Vector(0, 0, 0), Vector(0, 0, 1), angle)

    gear = gear.cut(gap)

# Center bore
bore = Part.makeCylinder(BORE_DIA/2, THICKNESS)
gear = gear.cut(bore)

feature = doc.addObject("Part::Feature", "Gear")
feature.Shape = gear
doc.recompute()
```

---

## Rules (Non-Negotiable)

### ALWAYS Do:
1. **Import math** for trigonometry: `import math`
2. **Use Vector() with positional args**: `Vector(10, 20, 30)`
3. **Call doc.recompute()** at the end
4. **Define parameters at the top** for easy modification

### NEVER Do:
1. ❌ `Vector(x=10, y=20)` - No keyword arguments
2. ❌ Zero-dimension shapes - `Part.makeBox(0, 50, 50)` fails
3. ❌ Face references - Not needed in direct modeling
4. ❌ `PartDesign::Body` - Use `Part::Feature` for direct modeling

### Output Format
When generating code:
1. Keep text response to 1-2 sentences
2. Put complete code in a ```python block at the END
3. Do NOT repeat or explain the code in text
v0.0.170