A Python-based, open-source 2D/2.5D CAM tool for CNC routers and mills.
PyMillCAM fills the gap between simple but limited tools like Estlcam and powerful but complex tools like Fusion 360's CAM. The goal is a wizard-driven tool for beginners that remains fully editable for experienced users.
Status: active early development. Phase 1 (foundations) is complete and most of Phase 2 has landed. The app is usable end-to-end for outside/inside profile cutouts (with leads, on-contour ramp entry, and tabs), pockets (offset / zigzag / spiral strategies, islands, rest-machining for V-notch corners), and drilling (simple / peck / chip-break cycles), exported as UCCNC G-code with shop-specific preamble / footer / tool-change macros. A shared tool library (JSON), Select Similar, operation duplication and reorder, per-op time estimates in the ops tree, and per-op G-code export are in, with UCCNC and GRBL post-processors both shipped (the post is picked automatically from the project's machine controller). Other posts (Mach3, LinuxCNC), wizards, safety checks, and built-in simulation are not built yet — see What's coming below.
- DXF import. Lines, arcs, circles, LWPOLYLINE with bulges. Arcs are preserved as arcs end-to-end (they survive into the G-code as G2/G3, not chord-approximated).
- Path stitching. Open segments that meet at their endpoints can be
welded with
Operations > Join paths, or automatically on import viaEdit > Preferences > Auto-stitch on DXF import. - Profile toolpath. Outside / inside / on-line offsets, climb or conventional direction, multi-depth stepping. Analytical arc-preserving offsetter for circles and line+tangent-arc contours (rounded rectangles); falls back to Shapely's buffer otherwise.
- Pocket toolpath. Three strategies — offset (concentric inward rings, arc-preserving for boundaries the analytical offsetter handles), zigzag (parallel raster strokes with a finishing contour ring, configurable angle), and spiral (same rings as offset but walked inner → outer with feed-at-depth bridges, so the path is a single continuous spiral from the pocket interior outward — lower cycle time, fewer floor witness marks). Multi-depth stepping with retract-to- clearance between passes. Ramp entries: linear (default — last slice of the first ring, so the full ring cuts flat at pass depth), helical (spiral tangent to ring-start); fallback chain helical → linear → plunge. Islands (holes inside the pocket) are detected from selection by a containment tree, with retract+rapid between disjoint ring groups. Rest-machining cleans up V-notch corners where an island grows close to the boundary.
- Drill operation. Three cycle types: simple (one plunge per hole), peck (full retract between pecks for chip clearance), chip-break (small in-hole retract to snap the chip). Drill targets can be POINT entities, closed circles, or closed contours — the engine resolves each target to a centre (exact for full-circle arcs; Shapely centroid for closed contours). Multi-hole per operation; between-hole traversal at the clearance plane. Expanded G0/G1 IR (not canned G81/G83) for post-processor portability.
- Profile tabs. Rectangular tabs, auto-spaced by arc-length, tuned
with count / width / height / ramp length. Multi-depth aware — on
passes that would cut through the tab, Z modulates to
max(planned_z, tab_z)so the tab plateau survives while lead-in/out and on-contour ramps still work normally. - Lead-in / lead-out. Arc, tangent, or direct styles, traversed at the stock surface (Z=0) so the plunge witness mark lands in air.
- On-contour ramp entry. Each pass descends along the contour at a fixed angle from the previous depth to the new depth — no plunging into material, no between-pass retract. After the final pass, a cleanup slice re-cuts the sloped groove at full depth and a fixed-angle ascent rises back to the surface before the lead-out.
- UCCNC and GRBL G-code output. Emits G2/G3 with helical Z for ramps,
feed modality, tool change and spindle commands. The post is selected
automatically from the project's machine
controller(pickuccncorgrblin the Machine dialog's controller dropdown). GRBL defaults to a manual tool-change pause (M5+M0) since stock GRBL has noM6handler; UCCNC emitsT<n> M6. Machine macros (program_start,program_end,tool_change) override those defaults per-project, so shops can swap in their own preamble, parking routine, and ATC sequences without forking the post.{tool_number}is substituted insidetool_change. - Tool library. JSON-backed (
~/.config/PyMillCAM/tool_library.json), atomic save (crash-safe). Edit > Tool library opens a dialog to add / duplicate / rename / delete entries. The Properties panel has a Tool dropdown on each operation; selecting a library tool locks the tool-geometry fields so edits happen in one place. - Select Similar. Right-click any entity (tree or viewport) → pick same layer / same geometry type / same diameter (circles only, 0.01 mm tolerance). Critical for selecting 200 identical mounting holes in one click.
- User-chosen contour start position (P₀). Right-click any entity that belongs to the active Profile/Pocket op → Set start position here. The engine rotates the offset contour / pocket rings so the cut begins at the nearest point on the toolpath — lead-in, on-contour ramp, SPIRAL entry, and tab bookends all land where you asked (typically in scrap). A yellow target marker shows the current position; Clear start position removes it. Pockets with islands keep the offsetter's default seam (documented limitation).
- Operation duplication & reordering.
Ctrl+Shift+Dclones the selected op with a unique(copy)/(copy N)suffix — useful for spot drill → peck drill → ream on the same holes, each with its own tool and cycle.Ctrl+Shift+Up/Ctrl+Shift+Downmove an op in the execution order; ops run top-to-bottom in generated G-code. - Per-op G-code. Select an op in the tree and press
Ctrl+Gto generate a standalone program for just that op (preamble + footer included); select the Operations group (or nothing) to generate the combined program. Right-click an op → Export G-code… writes that single op to a.ncfile. - Machine editor + library.
Edit → Machine…edits the project's machine — name, controller, and the three macro slots (preamble, footer, tool change) that the post substitutes into the program.{tool_number}expands inside Tool change. A Load from library picker at the top lets you swap the current project's machine to a different library entry without starting a new project.Edit → Machine library…opens the app-global library at~/.config/PyMillCAM/machine_library.json: add / duplicate / rename / delete machine entries and mark one as the default. New projects inherit the default machine (with a fresh id, so editing the project's copy never retro-propagates to the library). - Active-op geometry editing. Selecting an op tints its member
entities (green) in the viewport and the tree.
Shift+A/Shift+Radd/remove the current viewport selection to the active op — or use the same actions from the unified right-click menu on any entity. - Per-op time estimate. Each op row in the tree shows an
[hh:mm:ss]estimate (rapids + feeds + arcs + dwell + tool-change), recomputed on project change. - Measurement + feeds/speeds. Press
Mto enter measurement mode, click two points (snaps to entity endpoints / arc centres) to see the distance and XY delta in the status bar. Selecting a single entity shows its dimensions (line length, circle diameter, arc radius + sweep). The Tool Library dialog has a Calculate from material… button next to the cutting-data fields: pick a material from the built-in table and it fills in a starting RPM / feed using the classic Vc × 1000 / (π × D) formula. - PySide6 GUI. 2D viewport with pan / zoom / fit, directional box
selection (L→R contained, R→L crossing) with
Ctrl/Shiftmodifiers for multi-select, operations tree, Properties panel, G-code output pane, undo / redo with command coalescing on property edits, project save/load as JSON (.pmc), and a toolbar + keyboard shortcuts for the common actions (see below).
See docs/pymillcam_plan.md for the full roadmap.
Short version:
- FreeCAD
.fctb/.fctland LinuxCNC tool-table import into the tool library - Wizards (Sheet Cutout, Pocket, Drill Pattern, …) — scaffold in place
- Pre-flight safety (Z stack budget, travel, fixture collision)
- Built-in simulator
- Mach3 / LinuxCNC post-processors
PyMillCAM uses uv for dependency management.
# Install uv (once, if you don't already have it)
curl -LsSf https://astral.sh/uv/install.sh | sh # Linux/macOS
# Or: winget install --id=astral-sh.uv -e # Windows
# Clone and run
git clone https://github.com/andax/pymillcam.git
cd pymillcam
uv sync # creates .venv and installs all deps
uv run pymillcam # launch the GUINo separate install step — uv sync handles the virtualenv and deps in
one go.
- Python 3.11+ (installed automatically by
uv sync) - A desktop environment with Qt6 support (Linux, macOS, Windows)
uv run pymillcamFile → Open Project…(Ctrl+Shift+O) → selectexamples/circle_cutout.pmc.Operations → Generate G-code(Ctrl+G, or the play-arrow in the toolbar). The bottom pane fills with UCCNC G-code; the viewport shows the toolpath overlay in magenta.- Click the operation in the tree, then adjust values in the Properties
panel (cut depth, stepdown, lead-in style, ramp angle). The orange
preview updates live as you type; press
Ctrl+Gto regenerate the G-code.
This is the workflow for a basic job. It assumes you have a DXF with a closed contour — a circle, rectangle, or anything closed — and want to profile-cut it out of a sheet.
1. Import the DXF. File → Open DXF… (Ctrl+O). PyMillCAM reads
lines, arcs, circles, and bulged LWPOLYLINEs; arcs stay as arcs all the
way through to G2/G3 in the output. The viewport fits the drawing
automatically. If your DXF was drawn with separate unstitched lines
(common for exports from Inkscape and some CAD tools), turn on
Edit → Preferences → Auto-stitch on DXF import or run
Operations → Join paths (Ctrl+J) on the selected entities afterward.
2. Configure your machine (one-time). Open Edit → Machine library…, add a machine, set its controller (uccnc or grbl),
paste your preamble / footer / tool-change sequences into the three
macro boxes, and hit Set as default. New projects from then on
inherit that machine automatically. Use {tool_number} inside Tool
change to insert the target tool number — handy for both ATC and
manual-change setups. The current project's machine is editable via
Edit → Machine… for one-off overrides; those edits stay with the
project and don't retro-propagate to the library.
3. Select the geometry for the first operation. Drag a box in the
viewport (left-to-right selects fully-contained entities;
right-to-left selects anything crossed by the box) — or click
individual entities. Hold Ctrl or Shift to add to the selection.
The left-side tree mirrors what's selected.
4. Add an operation. Pick the op type that matches the cut:
- Profile (
Ctrl+P) — cut along a contour. Choose outside, inside, or on-line in the Properties panel. - Pocket (
Ctrl+K) — clear the area enclosed by a closed contour. Pick offset, zigzag, or spiral as the clearing strategy. - Drill (
Ctrl+D) — drill at a point, a closed circle centre, or the centroid of a closed contour. Pick simple, peck, or chip-break as the cycle.
The operation appears in the Operations tree group. Its member entities tint green in the viewport.
5. Set the operation parameters. With the op selected, the Properties panel on the right shows its fields. At minimum you need:
- Name — make it descriptive (
"Outer cutout","6mm dowel holes"). The name shows in the G-code as a comment header. - Tool — pick from the Tool dropdown (library tools) or leave as
(Custom)and edit the fields inline. - Cut depth (negative) — typically
-stock_thicknessfor a cutout,-3to-5 mmfor a pocket. - Stepdown — how deep each pass cuts. Enable Multi-pass if
cut_depth / stepdown > 1.
Profile-specific: offset side, lead-in/out style, ramp strategy, tabs. Pocket-specific: stepover, strategy, zigzag angle, ramp strategy. Drill-specific: cycle, peck depth, dwell.
6. Add more operations if needed. Repeat steps 3–5 for each
operation in the job. Reorder with Ctrl+Shift+Up / Ctrl+Shift+Down
(or via the op row's right-click menu) — ops execute top-to-bottom
in the generated G-code. A typical plate with mounting holes runs:
drill pilots → drill final → pocket recesses → profile cutout.
7. Generate G-code. Ctrl+G with nothing selected (or the
Operations group selected) writes a full program covering every op;
with one op row selected it writes just that op as a standalone
program. The viewport shows rapids as dashed cyan and feeds as solid
magenta.
8. Export G-code to your machine. The bottom pane's text is the
complete program — copy-paste, or right-click any op in the tree →
Export G-code… to write just that one op to a .nc file. Save
the whole project (Ctrl+S) so you can come back to it.
See examples/README.md for more sample files
and follow-ups (DXFs with islands, pocket + drill combos, etc.).
| If you want to… | Use | Key strategy options |
|---|---|---|
| Cut a contour free of the sheet | Profile | outside offset, arc lead-in, tabs |
| Clear area inside a closed shape | offset for arc-preserving rings, zigzag for flat floor, spiral for no-retract continuous path | |
| Drill a set of holes | Drill | simple for through-holes, peck for deep holes, chip-break for stringy chips |
Two things that cross all op types:
- Ramp entry (Profile / Pocket) — how the cutter enters each depth step. Helical for pockets with room; linear on-contour for open contours and tight pockets; plunge only for centre-cutting tools or pre-drilled starter holes. The engine falls back automatically (helical → linear → plunge) if the requested strategy doesn't fit.
- Multi-depth — break a deep cut into
stepdown-sized passes. Retracts to the clearance plane between passes, except for on-contour ramp which stays at depth.
| Action | Shortcut |
|---|---|
| New project | Ctrl+N |
| Open DXF | Ctrl+O |
| Open Project | Ctrl+Shift+O |
| Save / Save As | Ctrl+S / Ctrl+Shift+S |
| Undo / Redo | Ctrl+Z / Ctrl+Shift+Z |
| Preferences | Ctrl+, |
| Fit to View | F |
| Measure distance | M |
| Join paths | Ctrl+J |
| Add Profile | Ctrl+P |
| Add Pocket | Ctrl+K |
| Add Drill | Ctrl+D |
| Duplicate operation | Ctrl+Shift+D |
| Move operation up / down | Ctrl+Shift+Up / Ctrl+Shift+Down |
| Add to active op | Shift+A |
| Remove from active op | Shift+R |
| Delete operation | Del |
| Generate G-code | Ctrl+G |
PyMillCAM is beta software. CNC machines generate enough force to
destroy expensive stock, break tools, damage the machine, and injure
or kill the operator. Everything in the license's NO WARRANTY clause
applies to this project; the bullet points below are the plain-English
version of what that means in practice:
- Verify every program before you cut. Load the generated G-code into a simulator (CAMotics, UCCNC's preview, your controller's backplot) and confirm the toolpath matches your intent. Pay particular attention to rapids, plunges, safe heights, and that the tool never drops below the stock outside the cut region.
- Run the first cut in air — lift the tool to safe height or run without the stock clamped — to catch overtravel, wrong origin, and spindle direction issues before they mean anything.
- Match the machine to the G-code dialect. The Machine dialog's
Controller setting drives the post; pick the one that matches your
firmware (
uccnc,grbl) and confirm the preamble / tool-change macros suit your shop. - Use proper safety equipment — eye protection, hearing protection, dust collection for wood / MDF, coolant / mist for metals. Keep an emergency stop within reach.
- PyMillCAM authors and contributors are not liable for any damage to machines, workpieces, fixtures, tools, or persons arising from use of the software. By using PyMillCAM you accept full responsibility for the G-code that runs on your machine.
Every generated G-code program carries a beta notice in the header comments as a reminder. Don't strip them without thinking about what they say.
If you're trying PyMillCAM as a tester, things that are especially useful to hear about:
- DXFs that fail to import cleanly (attach the file).
- G-code that your controller doesn't accept or that cuts wrong —
attach the
.pmc, the DXF, and the generated G-code. - UI flows that are confusing or assume too much.
- What's the smallest real job you'd want to do with this, and what's blocking it today?
Please file issues on GitHub. For bug reports, a minimal .pmc that
reproduces the problem is worth 1,000 words.
uv run pytest # full test suite (fast — a couple of seconds)
uv run ruff check . # lint
uv run mypy src # strict type-checkArchitecture is documented in CLAUDE.md and in much more
detail in docs/pymillcam_plan.md.
GPL-3.0-or-later. Full text in LICENSE. PyMillCAM is
distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY — see Safety above for the plain-English
implications for CNC use.