Computer-Controlled Machining

07-computer-controlled-machining.org-IMG_20260310_104943.jpg

1. Week assignments

group assignment

  • do your lab's safety training
  • test runout, alignment, fixturing, speeds, feeds, materials, and toolpaths for your machine

individual assignment

  • make (design+mill+assemble) something big (~meter-scale)
  • extra credit: don't use fasteners or glue
  • extra credit: include curved surfaces
  • extra credit: use three-axis toolpaths

2. Prior Knowledge

I have not used a ShopBot or any CNC milling machine before.

3. Work!

Documentation of some Waag heroes I used for reference:

3.1. Group assignment

We started with safety and machine instructions. Henk showed us around the ShopBot and its dust collection installation.

Some facts about the ShopBot at the Waag:

  • It has 2.5 dimensions (no tilting).
  • X size is 2.5 meters.
  • Y size is 1.3 meters.
  • Z size is 20 centimeters.
  • Spindle maximum speed is 18,000 rpm.
  • We'll only be using a 5mm, 2-flute, flat-end milling bit for this week.
07-computer-controlled-machining.org-IMG_20260305_110617.jpg
Figure 1: ShopBot at the Waag (note the dangerously placed wood on the side!)

The machine has a large QR-code on the side pointing to How to use the ShopBot, which describes how to operate the machine very well. However, there are a couple of important things missing:

  • Always remove the milling bit first for inspection. There should not be any broken-off parts, it should be the bit you expect, and it should be fastened properly after inspection.
  • Clean the inside of the collet after removing the milling bit.
  • Check that nothing is leaning against the machine or on the bed before starting it.
  • In case of fire: turn the dust collector off and detach the bag (see figure 2). If there's smoke coming from the collection bag: open the window and throw the bag out the window. If there is still fire, use the fire extinguisher to put it out.

    07-computer-controlled-machining.org-IMG_20260305_112349.jpg
    Figure 2: Clamp holding the dust bag
  • Keep the door of the dust collector open; otherwise, the fire alarm may give a false alarm.
  • The X/Y axes are not what you expect. The X-axis goes from front to back and the Y-axis from left to right. Fortunately, the arrow keys on the keyboard when running the keypad in ShopBot console point in the right direction.

As an example Henk setup a job in VCarve Pro (version 8) on the computer attached to the ShopBot while we watched and took notes. There is also a nice VCarve tutorial which can be found on the site at the QR-code to supplement notes taken.

After detaching the milling bit for inspection and cleaning the collet. We cleaned the bed and placed a piece of plywood on it. Then, we positioned the milling bit at the bottom-right corner of the material by stepping to it using the "K" function in ShopBot Console. When it was positioned correctly, we used the Zero -> Zero X and Y axes tool to set the starting point and took a photo for when we need to restart the machine or computer for some reason, like a computer crash, hitting the Stop button etc.

07-computer-controlled-machining.org-screenshot-2026-03-08_17-25-47.png.jpg
Figure 3: Checking if the bit is at the corner of the material after a step
07-computer-controlled-machining.org-IMG_20260305_135630.jpg
Figure 4: Positioning the milling bit to the corner of the material

Henk showed us how to create some sample vectors in VCarve Pro and some profiles (all through, so depth set to the material thickness) and matching pockets toolpaths (half the material thickness). For all cuts cutting direction "climbing" was selected and for the pockets we had "offset" and "raster" clearing methods. For one of the pockets we used a higher feed rate of 80 mm/s than the default 60 mm/s (adjustable through Tool selection). For the profiles, tabs were applied to avoid parts coming loose during milling, and T- and dog- bone fillets to clear the corners (you can not make a sharp corner with a round bit).

07-computer-controlled-machining.org-IMG_20260305_130152.jpg
Figure 5: Measuring the material for job creation in VCarve

Before running the tests, the material needed to be screwed down to the sacrificial layer to make it a flat as possible (wood tends to warp) and avoid it from moving around. For this shallow drill holes were added to the job in VCarve and exported separately as an SPB-file to mark the places where the screws came. It is very important to not run this job again after the screws are in!

At first, we (mistakenly) set used the ShopBot console to Z-zero on top of the material. However, the job in VCarve was set to have the Z-zero on the bottom of the material. This resulted in a rather boring airshow. Fortunately, it was not the other way around.

07-computer-controlled-machining.org-screenshot-2026-03-08_17-46-59.png.jpg
Figure 6: Screwing down the material using the drill holes

After 10 minutes of milling, this is the result.

07-computer-controlled-machining.org-IMG-20260305-150633.jpg
Figure 7: Milled plywood (photo by Christian)

We repeated the run with some extra cuts on a board of OSB (Oriented Strand Board).

3.1.1. Test results

Using a feed rate of 80mm/s being a bit messier than the default 60mm/s. Comparing the plywood with the OSB, there was a much bigger difference in fraying (see figure 9). We could conclude this feed rate difference matters less than what the wood used. Even in a single slab of plywood, cuts in the same direction can be frayed on one part of the board and be fine on another.

A second test to include the spindle speed did not really reveal any sweet spot for that particular piece of wood. Although, I this case I would use 14k rpm and a feed rate of 40 mm/s.

07-computer-controlled-machining.org-IMG_20260311_104312.jpg
Figure 8: Spindle speed and feed rate tests

Apart from the fraying OSB the results we pretty similar between the OSB and the plywood. But, milling the OSB was a lot louder, probably because it contains more glue.

07-computer-controlled-machining.org-IMG-20260305-173932.jpg
Figure 9: Frayed OSB

After the speed tests we ran a runout test by projecting the shadow of the rotating bit on a piece of paper filming that.

At 1k rpm it is a bit wobbly.

The alignment between the X and Y-axis seem pretty good (see figure 10).

07-computer-controlled-machining.org-IMG-20260305-174616.jpg
Figure 10: Testing for alignment by checking the angle of a cut square

3.1.2. Steps to run a job

Take the following steps when using the ShopBot:

  1. Turn on the computer.
  2. Turn on the ShopBot.
  3. Remove the milling bit for inspection. See also: changing the milling bit.
  4. Clean the collet.
  5. Install the milling bit.
  6. Clean the sacrificial layer and remove any unevenness.
  7. Find and use the least damaged part of the sacrificial layer, which will be covered by the material, to do Z-leveling. See also: Zeroing the axes.
  8. Place the material on the bed.
  9. Open a new job in VCarve and enter the material dimensions. See also: Job setup.
  10. If the material is large enough to stay in place during dilling, create a drill path in VCarve for screwing down the material onto the sacrificial layer. Keep them a few centimeters away from the paths you will be cutting and make them a couple of millimeters deep. See also: Drilling toolpath.
  11. Check that nothing is left on the bed or leaning agains the machine.
  12. Zero the machine to the bottom-right edged of the material using XY in ShopBot Console toolbar. See also step about Z-leveling.
  13. Take a photo of the X and Y coordinates.
  14. Start the dust collector.
  15. Start the spindle. See also Starting the job.
  16. Select the drilling job as exported from VCarve.
  17. Run the job.
  18. Turn off the spindle and move the head away from the material.
  19. Screw down the material in the drilled holes.
  20. Start the spindle and run the job.
  21. Done? Stop the machine and vacuum, remove work from the bed, and clean up the mess!

3.2. Something big

I wanted to make a table to put my coffee on when I sit on the sofa, but I did not immediately decide next to which sofa it should go. So, I made it fully parametric: not only material thickness and but all dimensions.

table_height = 610;
table_width = 300;
table_depth = 780;

bit_diameter = 5;
thickness = 18;
thickness_offset = thickness * 1.5;

side_height = table_height * .25;
side_cut = 5.5;

$fn = 200;

module rounded_square(w, d, r) {
  translate([r, r, 0]) circle(r);
  translate([w - r, r, 0]) circle(r);
  translate([r, d - r, 0]) circle(r);
  translate([w - r, d - r, 0]) circle(r);
  translate([r, 0, 0]) square([w - r * 2, d]);
  translate([0, r, 0]) square([w, d - r * 2]);
}

module slot_mask(coordinates, bits = [true, true, true, true]) {
  r = bit_diameter * .75; // leave some room to turn
  square(coordinates);
  if (bits[0]) {
    translate([r / 2, r / 2, 0]) circle(r);
  }
  if (bits[1]) {
    translate([coordinates[0] - r / 2, r / 2, 0]) circle(r);
  }
  if (bits[2]) {
    translate([coordinates[0] - r / 2, coordinates[1] - r / 2, 0]) circle(r);
  }
  if (bits[3]) {
    translate([r / 2, coordinates[1] - r / 2, 0]) circle(r);
  }
}

module table_top() {
  difference() {
    rounded_square(table_width, table_depth, thickness * 2);

    // head slots
    hs = table_width / 2 - table_head_slot / 2;
    translate([hs, thickness_offset, 0])
      slot_mask([table_head_slot, thickness]);
    translate([hs, table_depth - (thickness_offset + thickness), 0])
      slot_mask([table_head_slot, thickness]);

    // side slots
    ss = table_depth / 2 - table_side_top_slot / 2;
    translate([thickness_offset, ss, 0])
      slot_mask([thickness, table_side_top_slot]);
    translate([table_width - thickness_offset - thickness, ss, 0])
      slot_mask([thickness, table_side_top_slot]);
  }
}

table_head_width = table_width - thickness_offset * 2;
table_head_slot = table_head_width / 2;

module table_head() {
  translate([thickness_offset, 0, 0]) difference() {
    union() {
      square([table_head_width, table_height - thickness]);
      polygon([[-thickness_offset, 0],
               [0, table_height - thickness - table_side_slot],
               [table_head_width, table_height - thickness - table_side_slot],
               [table_head_width + thickness_offset, 0]]);
      translate([table_head_width / 4, table_height - thickness, 0])
        square([table_head_slot, thickness]);
    }

    // top slots
    ts = table_head_width / 4;
    translate([0, table_height - thickness, 0])
      slot_mask([ts, thickness], [false, true, false, false]);
    translate([table_head_width - ts, table_height - thickness, 0])
      slot_mask([ts, thickness], [true, false, false, false]);

    // side slots
    ss = table_height - thickness - table_side_slot_offset - table_side_slot;
    translate([0, ss, 0])
      slot_mask([thickness, table_side_slot], [false, true, false, false]);
    translate([table_head_width - thickness, ss, 0])
      slot_mask([thickness, table_side_slot], [true, false, false, false]);

    // feet
    rr = table_head_width / 3;
    translate([table_head_width / 2, 0, 0]) circle(rr);

    // side
    n = floor((table_height - thickness) / (rr * 2.5) - .5);
    for (i = [1:n]) {
      translate([table_head_width / 2, rr * ((i * 2) + 0.5), 0])
        circle(rr * .66);
    }
  }
}

table_side_width = table_depth - (thickness_offset) * 2;
table_side_top_slot = table_side_width / 3;
table_side_slot = side_height * .5;
table_side_slot_offset = 0;

module table_side() {
  h = side_height;
  w = table_side_width;
  difference() {
    square([h, w]);
    translate([h * (side_cut + 0.5), w / 2, 0]) circle(h * side_cut);

    // top slot
    ts = table_side_width - table_side_top_slot * 2;
    translate([0, 0, 0])
      slot_mask([thickness, ts], [false, false, true, false]);
    translate([0, table_side_width - table_side_top_slot, 0])
      slot_mask([thickness, ts], [false, true, false, false]);

    // side slots
    translate([table_side_slot + thickness, 0, 0])
      slot_mask([table_side_slot, thickness], [false, false, false, true]);
    translate([table_side_slot + thickness, w - thickness, 0])
      slot_mask([table_side_slot, thickness], [true, false, false, false]);
  }
}

module preview() {
  color("#f00")
    translate([0, 0, table_height - thickness])
    linear_extrude(thickness)
    table_top();

  color("#ff0")
    translate([0, thickness_offset + thickness, 0])
    rotate([90, 0, 0])
    linear_extrude(thickness)
    table_head();

  color("#f0f")
    translate([0, table_depth - thickness_offset, 0])
    rotate([90, 0, 0])
    linear_extrude(thickness)
    table_head();

  color("#0f0")
    translate([thickness_offset, thickness_offset, table_height])
    rotate([0, 90, 0])
    linear_extrude(thickness)
    table_side();

  color("#00f")
    translate([table_width - thickness - thickness_offset,
               thickness_offset,
               table_height])
    rotate([0, 90, 0])
    linear_extrude(thickness)
    table_side();
}

%preview();

table_top();
translate([table_width + thickness_offset, 0, 0]) table_head();
translate([table_width + thickness_offset, table_height + thickness_offset, 0]) table_head();
translate([(table_width) * 2 + thickness_offset * 2, 0, 0]) table_side();
translate([(table_width) * 2 + side_height + thickness_offset * 2 + thickness, 0, 0]) table_side();
table.png

Features in the above model:

  • parameteric material thickness (duh!)
  • parameteric width, depth, height
  • "automatic" weight saving holes in the sides
  • includes "dog bone" fillets

I am very happy with the addition of dog bones in this model because it allowed me to inspect my design in the OpenSCAD viewer. The holes make it possible to see the pieces actually touch instead of being merged together.

07-computer-controlled-machining.org-screenshot-2026-03-08_13-31-30.png.jpg
Figure 11: Preview of dog bone fillets in OpenSCAD

3.2.1. Clearance test

Before milling the project, I wanted to do some press-fit tests. I used the model below to make profile toolpaths with different Clearance offset values.

bit_diameter = 5;
thickness = 18;

$fn = 200;

module slot_mask(coordinates, bits = [true, true, true, true]) {
  r = bit_diameter * .75;
  square(coordinates);
  if (bits[0]) { translate([r / 2, r / 2, 0]) circle(r); }
  if (bits[1]) { translate([coordinates[0] - r / 2, r / 2, 0]) circle(r); }
  if (bits[2]) { translate([coordinates[0] - r / 2, coordinates[1] - r / 2, 0]) circle(r); }
  if (bits[3]) { translate([r / 2, coordinates[1] - r / 2, 0]) circle(r); }
}

slot_mask([thickness, thickness * 3]);

translate([thickness * 2, 0, 0]) difference() {
  union() {
    square([thickness, thickness * 3]);
    translate([thickness, -.5 * thickness])
      square([thickness * 1.5, thickness * 4]);
  }
  translate([0, -thickness, 0]) slot_mask([thickness, thickness], [false, false, true, false]);
  translate([0, thickness * 3, 0]) slot_mask([thickness, thickness], [false, true, false, false]);
}
clearance.png

Former student Paola Zanchetta documented her work on doing a clearance test in her week 7 documentation. I started by trying her values.

3.2.2. Importing into VCarve

VCarve Pro can import PDF-files (not SVG). OpenSCAD allows exporting to PDF, but it only exports to paper sizes like A4, so it will not work for "something big". Fortunately, exporting to SVG, opening it in Inkscape and exporting there to PDF does produce a PDF with the correct dimensions.

table.svg
Figure 12: SVG of table

Before creating a new job in VCarve, I measured the piece of plywood I was going to use: 2000mm by 1000mm and 18mm (varying between 17.9 and 18.1) thick (see figure 13). For the zero position on the Z-axis, I selected the bottom of the material.

Next, I imported the clearance test and table PDFs I created. While importing I noticed the dog bones fillets VCarve creates are much wider than the bit diameter (see figure 14). I corrected my design to make the holes a bit bigger (because maybe I am too optimistic?), and re-imported them.

07-computer-controlled-machining.org-IMG_20260309_082239.jpg
Figure 13: Job setup.
07-computer-controlled-machining.org-IMG_20260309_124802.jpg
Figure 14: Hole diameter is 8.98mm using a 5mm milling bit

I had a bit of a struggle defining toolpaths and creating SPB-files. The icons in the toolbars are a bit confusing so it felt like feeling my way through the dark, but after a while, I got the hang of it.

I started with drill holes (5mm circles), which I placed around the clearance test shapes. For the toolpath I selected drilling using the already selected "Fab Academy End Mill (5mm)" option and make sure it was what I expected it to be (see figure 15).

07-computer-controlled-machining.org-IMG_20260309_092702.jpg
Figure 15: Fab Academy End Mill (5mm)

3.2.3. Milling

Ready to mill! I removed the bit to inspect it (see figure 16), reattached it, cleared the bed, z-zeroed on a good part of the bed, put the plywood on it (eye-ing the alignment with the bed), x-y-zeroed the bit on the bottom-right corner of the material, turned on the vacuum, started the spindle, loaded the drilling job into ShopBot Console, and ran it.

All went as expected. After the machine was done, I moved the head to the back (along the X-axis) to make some room to screw the board down using "woodies" (see figure 17).

07-computer-controlled-machining.org-IMG_20260309_093844.jpg
Figure 16: Bit disassembled
07-computer-controlled-machining.org-IMG_20260309_095808.jpg
Figure 17: Screwing down part of the board

Then, once it was screwed down, I copied the slot part of the test four times and gave them clearance offsets: 0, 0.1mm, 0.2mm, and 0.3mm. Satisfied, I started this job, but after a couple of passes on the first element of the test, I realized I forgot to add tabs. Afraid I would be catapulted to death by a loose piece of the wood, I stopped the job, adjusted the toolpaths to include tabs.

While it was cutting the last slot, I used calipers to measure the slot width; it was too narrow… I should have used negative offsets. And the skirt came loose (tighten that wing nut!).

I added new slots with negative offsets and ran it again.

After unscrewing the board and popping out the spoils, I tested the fit. Clearance offset -0.3mm gave the perfect fit (see figure 19).

07-computer-controlled-machining.org-IMG_20260309_103510-r90.jpg
Figure 18: Ready for testing
07-computer-controlled-machining.org-IMG_20260309_103903.jpg
Figure 19: Perfect fit using -0.3mm clearance offset

Next, I reinstalled the plywood on the bed (without screws!). I prepared a lot of drill holes around the table shapes, ran the job, and screwed the board down again (using only the new holes!).

07-computer-controlled-machining.org-IMG_20260309_112703.jpg
Figure 20: Drilling down the board
07-computer-controlled-machining.org-IMG_20260309_120949.jpg
Figure 21: Almost done

After 37 minutes of loud milling (and Così fan tutte on noise-cancelling headphones), it was done. I was at the Waag at 8:00, the job finished at 12:00. Henk was not kidding when he said it would take at least half a day with the CNC!

07-computer-controlled-machining.org-IMG_20260311_174553.jpg
Figure 22: Nicely fitting joint
07-computer-controlled-machining.org-IMG_20260309_141233.jpg
Figure 23: Still hanging in the tabs
07-computer-controlled-machining.org-IMG_20260309_144504.jpg
Figure 24: Lightly sanded and put together

4. Reflection

I was dreading this week because I had no idea what to make, and Fab Academy is taking its toll. The latter means I am tired (and the sawdust doesn't help) thus I am very wary of dangerous machines.

4.1. Good

I made furniture!

My son started vocational school in furniture making in September and ended up giving me some advice. He showed me a design of a small stool he should have made ("long story, dad", sigh/..) using a CNC, so that got my ideas brewing. Awesome!

4.2. Bad

It took a lot of time: both the modeling and the milling. I could get faster at this, but it is still a lot of work.

Also, I wasted too much wood. I was too afraid of things going wrong when putting parts closer together, but that's also something that comes with experience, I guess.

4.3. Ugly

It was very frustrating that I cannot run VCarve myself and thus prepare a lot at home. Also, its UI is very confusing and could lead to dangerous situations (like carving through screws inserted using drill holes).

5. Source files

Here are my source files.

Clearance tests:

Table:

Copyright © 2026 Remco van 't Veer

Licensed under a
CC BY-NC-SA 4.0

Build using
GNU Emacs, Org Mode and GNU Guix

Source code hosted at
gitlab.fabcloud.org