Skip to main content

A library for generating HSM tool paths for CNC pocket milling.

Project description

A library for generating HSM tool paths for CNC pocket milling.

TLDR

This project generates HSM (High Speed Machining) style CNC tool paths which minimise abrupt changes in tool engagement.

It is intended to be used as a library in a CAM package.

It is NOT intended to be used to directly generate gcode.

The problem

When CNC milling pockets, calculating a tool path that minimises endmill engagement and abrupt changes in engagement is challenging. There are few choices available to hobbyists and even less open source options.

Contour milling is the standard way of milling pockets where successive cuts run parallel to each other leading to high endmill engagement in tight corners and abrupt changes in endmill engagement.

This project generates a tool path which consists of a series of arcs. Each arc is centered on a point equidistant to two or more pockets edges and with an arc radius set so the pocket edge is a tangent to the arc. This leads to gradual increases in tool engagement, greatly decreasing the change of endmill damage.

Example path

Projects using this library

  • DerpCAM A 2.5D CAM aimed at hobby CNC machine users.

How to use this library

Basic calculation of an internal pocket:

./examples/example_basic.py ./test_cases/stegasorus.dxf

Calculation of an internal pocket starting from a pre-drilled hole:

./examples/example_with_predrill.py ./test_cases/triceratops.dxf

Calculation of an outside clearing operation where the edge of the stock is encoded as the outside polygon in the .dxf file:

examples/example_outer.py ./test_cases/hextest.dxf

Calculation of an outside clearing operation where the edge of the stock is calculated as a simple offset from the outer edge of the .dxf part file:

examples/example_refine.py ./test_cases/thrash.dxf

The main entry point is geometry.Pocket. A minimal integration looks like:

import ezdxf
from hsm_nibble import dxf, geometry
from hsm_nibble.arc_utils import ArcData, LineData, ArcDir, MoveStyle

shape = dxf.dxf_to_polygon(ezdxf.readfile("part.dxf").modelspace())

toolpath = geometry.Pocket(shape, step=1.0, winding_dir=ArcDir.CW)

for element in toolpath.path:
    if isinstance(element, ArcData):
        # arc cut — use origin, radius, start/end angles, winding_dir
        pass
    elif isinstance(element, LineData):
        if element.move_style == MoveStyle.RAPID_OUTSIDE:
            pass  # retract, rapid, plunge
        else:
            pass  # in-pocket connecting move

Output fields

toolpath.path is a list of ArcData and LineData elements (both are NamedTuples).

ArcData — a cutting arc:

Field Type Description
origin Point Arc centre
radius float Arc radius
start Point Start point on the arc
end Point End point on the arc
start_angle float Angle to start from centre (radians)
span_angle float Signed angular extent; negative = CW
winding_dir ArcDir CW or CCW
path LineString Discretised arc geometry (for geometry ops)

LineData — a connecting move:

Field Type Description
start Point Move start
end Point Move end
path LineString Line geometry
move_style MoveStyle CUT, RAPID_INSIDE, or RAPID_OUTSIDE

RAPID_OUTSIDE means the tool must retract above the part before the move and plunge again afterwards. RAPID_INSIDE is a fast move over already-cut material with no retract needed. CUT is a short feed-rate connecting move.

Streaming / progress reporting

For large or complex pockets, pass generate=True to stream arcs incrementally rather than blocking until the full path is computed. get_arcs(timeslice=N) yields a progress ratio (0.0–1.0) each time N milliseconds of computation have elapsed, so a GUI host can stay responsive:

toolpath = geometry.Pocket(shape, step=1.0, winding_dir=ArcDir.CW, generate=True)

for progress in toolpath.get_arcs(timeslice=200):
    print(f"{progress:.0%}")
    # toolpath.path contains all arcs computed so far

Pre-drilled holes and multi-pass operations

If the tool has already cut some area (e.g. a pre-drilled entry hole, or a prior roughing pass), pass it as already_cut so the planner avoids re-cutting it and routes the entry correctly:

from shapely.geometry import Point

entry_hole = Point(x, y).buffer(hole_radius)
toolpath = geometry.Pocket(shape, step=1.0, winding_dir=ArcDir.CW,
                           already_cut=entry_hole)

Other useful libraries in this repo

The VoronoiCenters class in hsm_nibble/voronoi_centers.py is a wrapper around the pyvoronoi package.
Unfortunately pyvoronoi only deals with straight lines and points. Not arcs.
The VoronoiCenters class prunes the Voronoi diagram and tries to do the same things when simplifying a Voronoi diagram created from geometry containing arcs and circles, similar to what the C++ Boost Voronoi library does for C++.


Architecture

See ARCHITECTURE.md for module dependency graph, class diagram, execution sequence, and an explanation of the arc-fitting bisection algorithm.


Testing

Unit tests (tests/unit/)

Fast, isolated tests for individual functions and classes — no DXF files required. Run them after any code change:

python3 -m unittest discover tests/unit/

Smoke tests (tests/integration/test_smoke.py, test_smoke_hard.py)

Quick sanity checks that run the full path planner against one or two real DXF files and assert high-level properties (non-empty path, no dangerous crashes, low path-fail ratio). They are slower than unit tests but much faster than the full coverage suite:

python3 -m unittest tests/integration/test_smoke.py tests/integration/test_smoke_hard.py

Coverage / integration tests (tests/integration/test_coverage.py, test_coverage_outer_peel.py)

Run the planner across all .dxf test cases in test_cases/ and measure how much of the pocket area is actually cut. These tests catch regressions in coverage quality but take significantly longer:

python3 tests/integration/test_coverage.py
python3 tests/integration/test_coverage_outer_peel.py

Convergence regression fixtures (tests/unit/test_arc_fitter_convergence.py)

Replay captured inputs where the arc-fitting bisection previously failed to converge. Ensures fixes stay fixed:

python3 -m unittest tests/unit/test_arc_fitter_convergence.py

Metrics runner (tests/integration/run_all.py)

Not a test suite — a CLI script that runs the planner over all DXF test cases and prints a table of quality metrics (uncut area, arc count, time per arc, etc.). Useful for manual performance comparisons:

python3 tests/integration/run_all.py
python3 tests/integration/run_all.py ./test_cases/square_circle.dxf --overlap 1.6 --winding CW

Area-diff visualiser (tests/integration/show_area_diff.py)

Renders a PNG (or interactive window) showing what the planner thinks was cut versus what the output arcs actually cover. Useful for diagnosing coverage gaps:

python3 tests/integration/show_area_diff.py test_cases/arcs.dxf --outdir /tmp/HSM
python3 tests/integration/show_area_diff.py test_cases/arcs.dxf --interactive

Linting

mypy ./hsm_nibble/

Install dependencies

Requires Python 3.7+.

pip install -r requirements.txt

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

hsm_nibble-0.2.4.tar.gz (542.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

hsm_nibble-0.2.4-py3-none-any.whl (48.0 kB view details)

Uploaded Python 3

File details

Details for the file hsm_nibble-0.2.4.tar.gz.

File metadata

  • Download URL: hsm_nibble-0.2.4.tar.gz
  • Upload date:
  • Size: 542.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for hsm_nibble-0.2.4.tar.gz
Algorithm Hash digest
SHA256 186653773a5eed5898df650e2e20a094a79edb5d3b006bac7d429e5a7521219c
MD5 908dff82eed900b0d8bc473b27316338
BLAKE2b-256 20cc6fd05bd9ce5ec3a901d4a4dfb889ac0a47ef350a132519430f64e2034536

See more details on using hashes here.

File details

Details for the file hsm_nibble-0.2.4-py3-none-any.whl.

File metadata

  • Download URL: hsm_nibble-0.2.4-py3-none-any.whl
  • Upload date:
  • Size: 48.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for hsm_nibble-0.2.4-py3-none-any.whl
Algorithm Hash digest
SHA256 a05e368aaeccd98d429061115740fa5f3fbda8a001c0595cd6723696e86b707a
MD5 dc91619447e8deeee150022abef91135
BLAKE2b-256 1ccdae98c03797a8a455fc019cf4d052576f46479b754a4a9357dc653c0c71ec

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page