RasmahRasmah

5-Axis Tool Orientation and Gouge Avoidance

Overview

A 3-axis mill keeps the cutter vertical, so a concave surface that is more curved than the tool gets gouged — the tool digs into the wall. A 5-axis machine solves this by tilting the cutter to follow the surface normal, choosing a lead/tilt orientation that clears the local curvature and avoids rear-gouge and holder collisions. This tutorial shows how Rasmah decides that orientation: it measures the surface curvature, rules out orientations that would gouge, and picks the safe orientation closest to the surface normal.

The worked part is the classic 5-axis job: a die block — a 40 × 40 × 20 mm block of tool steel with a Ø30 mm spherical cavity scooped into the top face — finished with a Ø8 mm ball-nose mill. The cavity floor wants a vertical tool; its steep 60° rim wants the tool tilted onto the wall normal, and not every tilt is safe.

The key physical fact: a ball-nose cutter of radius R is a sphere, so its contact curvature is 1/R in every direction. It locally gouges a surface whose principal curvature is more concave than the ball — i.e. κ < -1/R.

using Rasmah
using LinearAlgebra

mm(v) = round(uconvert(u"mm", v); digits = 1)
mm (generic function with 1 method)

Drawing the die block

The block is box − sphere: a 40 × 40 × 20 mm box with a Ø30 mm sphere whose centre sits 12 mm above the base centreline — so the cavity runs 13 mm deep and its mouth is Ø29.7 mm across. to_sdf turns the feature into the callable field the curvature analysis samples:

part = difference(box(0.040, 0.040, 0.020), translate(sphere(0.015), 0.0, 0.0, 0.012))
g = to_sdf(part)

floor = [0.0, 0.0, -0.003]                       # the deepest point of the cavity
θ = deg2rad(60.0)
rim = [0.015 * sin(θ), 0.0, 0.012 - 0.015 * cos(θ)]   # a point on the 60° wall
println("rim: ", mm.(rim), " mm   on surface: ", round(g(rim); digits = 6))
rim: [13.0, 0.0, 4.5] mm   on surface: -0.0
vtk(surface_mesh(part; n = 48))

Curvature of a surface

surface_principal_curvatures reads the two principal curvatures κ₁ ≥ κ₂ of an implicit surface from the signed-distance field’s Hessian. Convex surfaces are positive, concave are negative. A sphere of radius r gives (1/r, 1/r), a cylinder (1/r, 0), a plane (0, 0):

println("R50 sphere   : ", round.(surface_principal_curvatures(to_sdf(sphere(0.05)), [0.0, 0.0, 0.05]); digits = 1))
println("R50 cylinder : ", round.(surface_principal_curvatures(to_sdf(cylinder(0.05, 0.1)), [0.05, 0.0, 0.0]); digits = 1))
R50 sphere   : (20.0, 20.0)
R50 cylinder : (20.0, 0.0)

The concave cavity scooped from the block has negative curvature — -1/r for the Ø30 mm sphere, ≈ -66.7 in m⁻¹:

κ1, κ2 = surface_principal_curvatures(g, floor)
println("cavity floor : ", round(κ1; digits = 1), ", ", round(κ2; digits = 1), "  (= -1/0.015)")
cavity floor : -66.7, -66.7  (= -1/0.015)

Does the ball gouge locally?

ball_local_gouge(κ₁, κ₂, R) is the curvature-interference check: it is true when the surface is more concave than a ball of radius R (i.e. min(κ₁, κ₂) < -1/R). The Ø8 mm finishing ball fits the Ø30 mm cavity; a Ø40 mm ball would not:

println("R = 4 mm fits    : ", !ball_local_gouge(-1 / 0.015, -1 / 0.015, 0.004))
println("R = 20 mm gouges : ", ball_local_gouge(-1 / 0.015, -1 / 0.015, 0.020))
R = 4 mm fits    : true
R = 20 mm gouges : true

Choosing a gouge-free orientation

gouge_free_orientation searches the lead/tilt angles (within the machine’s inclination limit) for an orientation that satisfies the curvature check, does not rear-gouge, and does not collide the holder — and returns the one closest to the surface normal. On the cavity floor the answer is vertical; on the 60° rim the answer tilts to follow the wall normal:

R = 0.004
a_floor = gouge_free_orientation(g, floor, [1.0, 0.0, 0.0], R; grid = 9)
println("floor axis : ", round.(a_floor; digits = 3))          # ≈ [0, 0, 1]

n = gradient(g, rim); n ./= norm(n)
a_rim = gouge_free_orientation(g, rim, [0.0, 1.0, 0.0], R; grid = 9)
println("rim normal : ", round.(n; digits = 3))
println("rim axis   : ", round.(a_rim; digits = 3))            # ≈ the wall normal
println("a_rim ≈ normal: ", norm(a_rim .- n) < 1e-3)
floor axis : [0.0, 0.0, 1.0]
rim normal : [-0.866, 0.0, 0.5]
rim axis   : [-0.866, 0.0, 0.5]
a_rim ≈ normal: true

The steep rim is where the tilting earns its pay: only 54 of the 81 lead/tilt candidates clear the wall without rear-gouging or holder collision — a vertical 3-axis finish would gouge here:

fm = feasible_orientation_map(g, rim, [0.0, 1.0, 0.0], R; grid = 9)
println(count(fm), " of ", length(fm), " orientations are feasible")
54 of 81 orientations are feasible

A full gouge-aware 5-axis finish

five_axis_finish_gouge_free rasters the surface and picks the safe orientation at every contact point, returning the tool path and the per-move tool axes:

tp, axes = five_axis_finish_gouge_free(surface_mesh(part; n = 48), BallEndMill(2R); stepover = 0.004, grid = 5)
oriented = [a for a in axes if a !== nothing]      # drop infeasible points
println("moves: ", num_motions(tp), " (", length(oriented), " with an orientation)")
println("all axes unit-length: ", all(abs(norm(a) - 1.0) < 1e-9 for a in oriented))
moves: 750 (750 with an orientation)
all axes unit-length: true

Summary

  • surface_principal_curvatures measures the surface (convex > 0, concave < 0).
  • ball_local_gouge(κ₁, κ₂, R) is the κ < -1/R curvature-interference rule.
  • gouge_free_orientation / feasible_orientation_map / five_axis_finish_gouge_free choose the gouge- and collision-free orientation closest to the surface normal.

Next steps