CNC Toolpaths and 3D-Print Slicing
Overview
A design on its own is geometry; a fabricated part is geometry plus a plan for the machine that makes it. CNC milling removes material with a spinning cutter that must be steered over every region to be cleared; 3D printing deposits material layer by layer, so the plan is a stack of 2D perimeters and fill patterns. Both are, at heart, the same idea: turn a solid into an ordered sequence of tool positions — a toolpath — and then serialize it to the machine’s G-code.
Rasmah models this with one neutral data structure, the ToolPath: a tool plus an ordered list of motions (rapid, linear, arc, helical), each carrying a cutting position. A milling strategy (pocketing, profiling, drilling) produces a ToolPath; a post-processor consumes one to write G-code; the stock simulator consumes one to subtract the swept tool volume from the workpiece.
using Rasmah
using LinearAlgebraA part to machine
Take a box and slice its top face to get the pocket region — the 2D loop(s) a 2.5D tool has to clear. slice_contours intersects a mesh with a horizontal plane and returns closed perimeters.
part = box_mesh([-0.01, -0.01, -0.01], [0.01, 0.01, 0.01]) # 20 mm cube
region = slice_contours(part, 0.01) # outline at the top
println("loops: ", length(region))
println("loop area: ", contour_area(region[1]), " m²") # 0.0004 = 20×20 mmloops: 1
loop area: 0.0004 m²
The region is one square loop. vtk shows the solid we will machine:
vtk(part)A pocket: clear the interior
A pocket removes everything inside the region down to a floor. pocket_zigzag does this with a rectilinear raster, stepping down in stepdown increments and finishing with a pass along the wall. A machinist first sets the speeds and feeds: for a 4 mm carbide end mill in aluminum, the catalog gives about 16 000 rpm and 3.2 m/min — a 50% stepover is a roughing pass:
tool = EndMill(0.004) # 4 mm flat end mill
rpm, feed_mmin, sfm = recommended_cut(:aluminum, 0.004)
tp = pocket_zigzag(region, tool; depth=0.002, stepover=0.002, stepdown=0.002,
feedrate=feed_mmin / 60, spindle=rpm)
println("motions: ", num_motions(tp))
println("path length: ", round(ustrip(toolpath_length(tp)); digits=4), " m")
println("rpm: ", round(rpm; digits=0), ", feed: ", round(feed_mmin * 1000; digits=0), " mm/min")
println("cutting time: ", round(toolpath_time(tp); digits=1), " s")motions: 49
path length: 0.521 m
rpm: 15915.0, feed: 3183.0 mm/min
cutting time: 10.6 s
The feed follows from the chip-load kinematics — v = π d N for the surface speed, feed = chip load × N × teeth — applied to the catalog values for the material; see the Feeds and Speeds tutorial for the full machining plan.
Every motion is either a RapidMove (position at speed) or a LinearMove (cutting). The first and last motions are retracts to a safe height:
println(first(tp.motions) isa RapidMove) # true
println(last(tp.motions) isa RapidMove) # truetrue
true
G-code for any controller
The same toolpath serializes to any dialect. Fanuc uses O1000 + (...) comments, Marlin uses ; comments and absolute extrusion:
g = gcode(tp; dialect=:fanuc)
print(first(split(g, "\n"), 6))SubString{String}["%", "O1000", "G90", "G21", "G17", "( Rasmah )"]
A drilling cycle on the same part: drill emits a peck cycle (full retract every peck):
d = Drill(0.002)
holes = [[0.005, 0.005, 0.01]]
td = drill(holes, d; depth=0.008, peck=0.004)
println("peck motions: ", num_motions(td))
println("bottom z: ", minimum(m.target[3] for m in td.motions))peck motions: 5
bottom z: 0.002
Verify by removing material
The stock simulator subtracts the tool’s swept volume from the workpiece, so the removed volume is exactly the material the tool cleared. A single plunge of a flat end mill removes a cylinder, so removed_volume matches π r² h:
stock = stock_box([-0.01, -0.01, -0.01], [0.01, 0.01, 0.01])
t = EndMill(0.004)
plunge = ToolPath(t)
push_rapid!(plunge, [0.0, 0.0, 0.02])
push_cut!(plunge, [0.0, 0.0, 0.008]) # 2 mm into the top
rv = removed_volume(stock.mesh, plunge; n=64)
println("removed: ", rv, " m³")
println("π r² h: ", π * 0.002^2 * 0.002, " m³")removed: 2.4869802775864293e-8 m³
π r² h: 2.5132741228718345e-8 m³
3D printing: slice and print
The additive side slices a solid into layers, then writes perimeters and infill with an extrusion axis. print_plan slices; print_gcode emits the full Marlin program (home, heat, then G1 … E… extrusion moves). zmin/zmax restrict the print to the middle 10 mm band of the 20 mm cube; at a realistic 0.2 mm layer height that is 51 layers:
plan = print_plan(part; layer_height=0.2e-3, zmin=-0.005, zmax=0.005,
perimeters=2, infill_density=0.2)
pg = print_gcode(plan; bed_temp=60, hotend_temp=210)
println("layers: ", length(plan.layers))
println("lines of g-code: ", length(split(pg, "\n")))
println(occursin("M109", pg), " (wait hotend), ", occursin("E", pg), " (extrusion)")layers: 51
lines of g-code: 2767
true (wait hotend), true (extrusion)
vase_mode instead prints a single continuous spiral wall, useful for vases and thin shells — one 0.2 mm lap at a time:
vase = vase_mode(part; layer_height=0.2e-3)
println("vase motions: ", num_motions(vase))vase motions: 801
Summary
- A
ToolPath(tool +RapidMove/LinearMove/ArcMove/HelicalMove) is the neutral representation; strategies produce it,gcodeserializes it, andsimulate_stockverifies it. - Milling strategies (
pocket_zigzag,profile_milling,drill,adaptive_clearing,parallel_finish,turning_*,wire_edm,laser_profile) and printing (print_plan,print_gcode,vase_mode) all emit the sameToolPath. - Verification is closed-form: pocket area,
π r² hremoved volume, extrusion volume balance — not just “it runs”.
Next steps
- Boundary Representation (BRep) — the exact topological model.
- Mesh and SDF — signed fields and marching cubes.
- Half-Space Slicing and Polyhedral BReps — the exact planar cut that underlies slicing.
