CNC Machining
How Phasio prices CNC machined parts using stock-block selection, a three-phase milling model (coarse, medium, fine), precision and quantity multipliers, and geometry-based review gates.
This equation is a starting point, not a production-ready solution
CNC pricing depends heavily on your specific situation: your machine inventory, past job data, material contracts, surface finish requirements, and GD&T callouts (tolerances, datums, fits). A part with a tight positional tolerance or a mirror-polished bore costs fundamentally more to machine than the geometry alone suggests. Calibrate the rates in this equation against real jobs before relying on it for live quoting.
This area is under active development
Phasio is introducing AI-powered feature recognition embedded in the quoting workflow - automatically detecting features like threads, bores, chamfers, pockets, and rotational symmetry directly from the 3D model geometry. The equations here will evolve significantly as these capabilities roll out.
Milling vs. turning
This equation prices milling by default, but turning (lathe) can be switched on programmatically based on geometry. A cylindrical part has two cross-sectional dimensions that are approximately equal - if width ≈ height, the part is likely rotationally symmetric and turning pricing applies.
// Detect likely turned parts: cross-section is roughly circular
const crossSectionRatio = Math.min(width, height) / Math.max(width, height)
const isLikelyCylindrical = crossSectionRatio > 0.85Once a part is flagged as cylindrical, the stock and pricing logic changes:
- Raw stock: a bar rather than a block -
Math.PI * Math.pow(Math.max(width, height) / 2, 2) * length - Machining: dominated by turning speed, number of diameter steps, and facing passes rather than three-phase milling
- Complexity: driven by how many diameter changes, undercuts, and threading operations are visible in the geometry
We've had good results pricing turned parts using this geometry-based switching approach. The key is using the cylindrical detection as a gate: parts that pass it get turning logic, everything else gets the milling model below.
Coming soon
As Phasio's AI feature recognition matures, this detection will happen at the platform level - recognising rotational symmetry, thread features, and bore patterns from the model directly rather than inferring from bounding box dimensions.
Why this process is unique
CNC is subtractive: you start from a block of raw stock and remove everything that isn't the part. Unlike additive processes, cost is not primarily about part volume - it's about how much you remove, and how hard that removal is.
Not all material removal is equal. Roughing through open space is fast. Working close to the finished surface requires smaller tools, slower feeds, and more passes. This suggests splitting the removed volume into three phases based on geometric complexity:
| Phase | Volume removed | Nature of work |
|---|---|---|
| Coarse | Block − convex hull | Bulk removal, large tooling, high MRR |
| Medium | Convex hull − shrink wrap | Intermediate features, recesses, overhangs |
| Fine | Shrink wrap − part volume | Surface finish, detailed features, toleranced geometry |
The convex hull is the coarse outer envelope of the part - all concave features are filled in. The shrink wrap follows the surface more tightly, capturing recesses but not sharp internal corners. Together they give you three geometrically-grounded milling volumes from fields Phasio already computes.
Factors geometry alone cannot capture - these require calibration or manual review:
- GD&T callouts: tight tolerances, datum schemes, press fits, true position
- Surface finish: Ra requirements, polishing, hard anodise, plating
- Multi-axis setups: 5-axis simultaneous, turn-mill, multiple fixturings
- Hard alloys: Inconel, titanium, hardened steel all reduce MRR significantly
Core methodology
Step 1 - Stock selection: filter your available block sizes to those that fit the part in any orientation, then pick the smallest. This determines raw material cost and the coarse milling volume.
Step 2 - Three milling phases: each phase uses a different cost-per-mm³ reflecting its MRR.
Why cost-per-mm³ rather than MRR × hourly rate
The equation uses direct cost-per-mm³ rates rather than MRR × hourly rate. The two are equivalent - e.g. 500 mm³/s at €72/hr = €0.00004/mm³. Volume rates are easier to calibrate from real job data.
Step 3 - Adjustments: precision multiplier (GD&T/tolerance level), quantity curve, minimum price floor.
Review gate: if the part doesn't fit any block, done(0) is called - a price of 0 automatically triggers reviewRequired, sending the order to manual review.
Numerical example
Part: 70 × 35 × 35mm bracket, volume = 15,000 mm³, convexHullVolume = 25,000 mm³, shrinkWrapVolume = 18,000 mm³
Smallest fitting block: 75 × 75 × 75mm = 421,875 mm³
| Phase | Volume | Rate | Cost |
|---|---|---|---|
| Coarse | 421,875 − 25,000 = 396,875 mm³ | €0.00004/mm³ | €15.88 |
| Medium | 25,000 − 18,000 = 7,000 mm³ | €0.0003/mm³ | €2.10 |
| Fine | 18,000 − 15,000 = 3,000 mm³ | €0.001/mm³ | €3.00 |
| Total | €20.98 |
With a precision multiplier of 1.0 and quantity of 1 (multiplier 3× at single unit): €62.94. Apply your minimum price floor as appropriate.
Complete algorithm
const { material, width, height, length, volume, convexHullVolume, shrinkWrapVolume, precision } = specification
const { quantity } = requisition
// Define your available raw stock sizes [x, y, z] in mm
// Add or remove blocks to match what your workshop actually carries
const blockSizes: [number, number, number][] = [
[25, 25, 25],
[50, 50, 50],
[75, 75, 75],
[100, 100, 100],
[150, 125, 125],
[150, 150, 150],
[200, 200, 200],
]
// Check if a part fits a block in any orientation (sorts both ascending)
function canFit(spec: [number, number, number], block: [number, number, number]): boolean {
const s = [...spec].sort((a, b) => a - b)
const b = [...block].sort((a, b) => a - b)
return s.every((d, i) => d <= b[i])
}
function blockVolume(b: [number, number, number]): number {
return b[0] * b[1] * b[2]
}
// Find the smallest block the part fits in
function findBlock(): [number, number, number] | null {
const fits = blockSizes.filter(b => canFit([width, height, length], b))
if (fits.length === 0) return null
return fits.reduce((smallest, b) => blockVolume(b) < blockVolume(smallest) ? b : smallest)
}
const bestBlock = findBlock()
// Cost rates per mm3 for each milling phase
// Calibrate these from your real job data
const coarseRate = variable('Coarse cost per mm3', 0.00004) // bulk removal
const mediumRate = variable('Medium cost per mm3', 0.0003) // intermediate features
const fineRate = variable('Fine cost per mm3', 0.001) // finishing and toleranced surfaces
const minPrice = variable('Min price per part', 1000) // floor - adjust to your cost base
// Quantity multiplier: interpolates smoothly between tiers
// At qty=1 price is 3x the base; converges to 1x at qty=1000
function getQuantityMultiplier(qty: number): number {
const tiers: [number, number][] = [
[1, 3.0],
[10, 2.3],
[100, 1.9],
[250, 1.6],
[500, 1.4],
[750, 1.1],
[1000, 1.0],
]
if (qty <= 1) return tiers[0][1]
if (qty >= 1000) return tiers[tiers.length - 1][1]
for (let i = 0; i < tiers.length - 1; i++) {
const [lo, loM] = tiers[i]
const [hi, hiM] = tiers[i + 1]
if (qty >= lo && qty <= hi) {
const t = (qty - lo) / (hi - lo)
return loM + (hiM - loM) * t // linear interpolation
}
}
return tiers[0][1]
}
if (bestBlock) {
const rawBlockVolume = blockVolume(bestBlock)
// Three milling phases
const coarseVolume = rawBlockVolume - convexHullVolume // bulk removal
const mediumVolume = convexHullVolume - shrinkWrapVolume // intermediate features
const fineVolume = shrinkWrapVolume - volume // finishing
const machiningCost = (coarseVolume * coarseRate)
+ (mediumVolume * mediumRate)
+ (fineVolume * fineRate)
// Precision multiplier: configure your precision settings in Phasio to return
// a value >= 1 (e.g. Standard = 1.0, Fine = 1.5, Ultra = 2.0)
const precisionMultiplier = precision?.value ?? 1
const qtyMultiplier = variable('Quantity multiplier', round(getQuantityMultiplier(quantity), 3))
const calculated = round(machiningCost * precisionMultiplier * qtyMultiplier, 2)
const finalPrice = variable('Final price', calculated < minPrice ? minPrice : calculated)
done(finalPrice)
} else {
// Part doesn't fit any block - done(0) triggers reviewRequired automatically
done(0)
}Material variables: none required - all rates are exposed via variable() for per-order override. Move rates to material.variables if they differ per material.
When to use this
3-axis and 5-axis CNC milling. Adjust the three cost rates to reflect your machine's actual MRR and hourly rate for each phase. Harder materials (Inconel, titanium) need lower MRRs - increase mediumRate and fineRate accordingly. Softer materials (aluminium, plastics) can use higher MRRs and lower rates.
Last updated on