Computer-Controlled Machining
This week I made a rickshaw out of OSB boards cut with ShopBot. The assembly is purely wooden and press-fit, and was (again) generated with a JavaScript program (online demo here).
The prompt is to “make something big”, which is slightly misleading since we're only allowed one 96x48in OSB board: just about enough material to make a plain 0.5m^3 cube (unless we buy our own wood)!
Initially I wanted to make a folding screen, on which intricate patterns will be engraved. However, I deemed the structure (4 panels hinged together) a bit too unchallenging. Then I considered a bit about making a coffin. But when I attended the shop training and got a sense of the material's dimensions, I realized it would have to be a very small coffin and not very, uh, comfortable for the deceased.
Thinking for a while I came up with an even better idea: a rickshaw. I'm referring to those dragged by a person on foot. They seemed to be popular in old photographs and texts, but have fallen out of fashion these days (in most places). I thought it would be pretty absurd and fun to make one myself. Plus it would be a worthy challenge, with wheels and stuff.
Design
Tom our shop admin has this to say about OSB boards:
If you make something skinny it will fall apart in your hands, and you will cry.
I tried to bear that in mind when using my “JavaScript-Aided-Design”(JAD) approach for making the design.
While for the laser cutter week, I computed most outlines as vectors and used raster+tracing only for the decorations, this time I decided to generate the entire design as raster and trace it at the final step. The reasons:
- Since the project is of a bigger scale using rough material, we won't need infinite resolution, and the accuracy can always be increased by tracing a larger raster image.
- While I used simple rectangles for joint slots last time, the slot shape required for shopbot is more complex: it needs to have a “dog bone” end so that the round tool can go in and fully clear the interior corners. I also wanted to add a chamfer to the opening of the slots, to make assembly easier. Adding these in vector is a pain, drawing them in raster is a breeze.
- mods, which we used to process our PCB designs, also uses the raster+tracing approach.
I made a trick, where in the raster, the slots, the dog bones and the chamfers have different colors from the background. This way, I can do threshold on different channels to optionally include these features in the exported vector.
The raster+tracing approach sped up my process by a large factor. Soon I got most of my parts described, but it turned out that this time, the main challenge was to fit the pieces onto a 96x48in board. Since a rickshaw involves a seat, it has to be of enough width on X and Z axes (otherwise it won't fit your butt), and of enough height on Y (otherwise instead of sitting on the chair you would be just squatting on the ground).
Initially my design involved a 2-layered wheel on each side to increase robustness, but it seemed that there was no way I could fit four wheel shapes on the board. So I reduced both to 1 layer; but then the slots for the caps that holds the wheels on either side would be too close, and the middle part seemed prone to breaking off. I ended up making the second layer a much smaller “ring” whose only function is to space out the slots.
I did a lot of similar compromises here and there and finally, it fits into the board, after some intense packing optimization (by hand). None of the pieces can grow an inch in any direction, and according to my calculations the product should be just large enough to fit the butt of a moderately sized human.
Still, I had to forego the front piece of the seat. There was just no space for it. I figured that the structure should be strong enough to go without it, and that the hollow can be used to stow some luggage, which is actually nice.
The next step was to create some decorations. I tried a couple of deterministic patterns (you can still find them in my code), but ended up making a randomly generated one, which I call “swirly nothings”. It's just a bunch of swirls stuck together. I put the pattern on the back of the seat, because it's the only piece with visible large empty area and doesn't have to physically withstand a lot of pressure (If the passenger does't try to lean too much on it that is).
The final design is the SVG below (accurate to 1/6 of a milimeter):
Similar to last time, I used a ruby script to import my design to SketchUp for preview and virtual assembly. I've improved the script so that it automatically deals with holes (since there're a lot of them this time) by checking the signed area of the polygons and extrude/un-extrude accordingly. I also made my JavaScript program generate the ruby script programmatically, filling in all the parameters from its parent code, and directly write into SketchUp's plugin folder. Hacky, but it's my style.
Below is an animation of the virtual assembly:
(You will see later that in real life, it is far less convenient to assemble these pieces!)
Full code below:
// rickshaw.js
// unit: mm
var tool_r = 4;
var slot_w = 11;
var slot_slack = 1;
var whl_r0 = 230;
var whl_r1 = 180;
var whl_r2 = 120;
var n_spoke = 21;
var spoke_w = 28;
var whl_sp = 1;
var axle_w = 72;
var axle_d = 80;
var axle_r = 75;
var axle_l = 740;
var axle_cap_r = 105;
var axle_sp = 35;
var frame_sp0 = 150;
var frame_sp1 = 300;
var frame_sp2 = 200;
var frame_slot_l = 40;
var box_l = 480;
var box_w = 405;
var side_bot_p = 85;
var side_back_p = 45;
var box_h = 300;
var arm_h = 200;
var seat_teeth_l = 100;
var handle_w = 50;
var handle_h = 300;
var handle_l = 780;
var handle_fl = 200;
var handle_fw = 64;
var handle_slot_l = 70;
var handle_bp = 30;
var handle_x = 100;
var handle_y = 80;
var seat_y = 20;
var pat_p = 35;
var teeth_ex = 1;
var SCALE = 2;
const W = 2438; //mm
const H = 1219;
const pattern = pattern2;
const TRACE_BONE = true;
const is_node = typeof process != 'undefined';
//sketchup extension
let su_rb_path;
let su_rb_fname = 'rickshaw.rb';
let fs;
let createCanvas;
let findContours,approxPolyDP;
if (is_node){
if (process.platform == 'win32'){
su_rb_path = require('os').homedir()+'\\AppData\\Roaming\\SketchUp\\SketchUp\ 2017\\SketchUp\\Plugins\\'+su_rb_fname
}else if (process.platform = 'darwin'){
su_rb_path = require('os').homedir()+'/Library/Application\ Support/SketchUp\ 2017/SketchUp/Plugins/'+su_rb_fname
}
fs = require('fs');
;({createCanvas} = require('node-canvas'));
;({findContours,approxPolyDP} = require('./findcontours.js'));
}else{
createCanvas = function(w,h){
let cnv = document.createElement('canvas');
cnv.width = w;
cnv.height = h;
return cnv;
}
;({findContours,approxPolyDP} = FindContours);
}
const PI = Math.PI;
const cos = Math.cos;
const sin = Math.sin;
let jsr = 0x5EED;
function rand(){
jsr^=(jsr<<17);
jsr^=(jsr>>13);
jsr^=(jsr<<5);
return (jsr>>>0)/4294967295;
}
var cnv = createCanvas(W*SCALE,H*SCALE);
let ctx = cnv.getContext('2d');
function fg(){
ctx.fillStyle="white";
ctx.strokeStyle="white";
}
function bg(){
ctx.fillStyle="black";
ctx.strokeStyle="black";
}
function circle(x,y,r,a0=0,a1=PI*2){
ctx.beginPath();
ctx.arc(x,y,r,a0,a1);
ctx.fill();
}
function line(x0,y0,x1,y1){
ctx.beginPath();
ctx.moveTo(x0,y0);
ctx.lineTo(x1,y1);
ctx.stroke();
}
function slot(x,y,l,ang,chamf=true){
l += slot_slack;
ctx.save();
ctx.translate(x,y);
ctx.rotate(ang);
bg();
ctx.fillStyle="red";
circle(l-tool_r,-slot_w/2,tool_r);
circle(l-tool_r, slot_w/2,tool_r);
if (chamf){
ctx.beginPath();
ctx.moveTo(slot_w,0);
ctx.lineTo(0,slot_w);
ctx.lineTo(0,-slot_w);
ctx.fill();
}
ctx.fillStyle="blue";
ctx.fillRect(0,-slot_w/2,l,slot_w);
ctx.restore();
}
function teeth(x,y,l,ang){
ctx.save();
ctx.translate(x,y);
ctx.rotate(ang);
bg();
ctx.save();
ctx.fillStyle="red";
ctx.globalCompositeOperation="multiply";
circle(0,-l/2-tool_r,tool_r);
circle(0, l/2+tool_r,tool_r);
ctx.restore();
fg();
// ctx.fillStyle="white";
ctx.fillRect(0,-l/2,slot_w+teeth_ex,l);
ctx.restore();
}
function wheel(){
fg();
circle(0,0,whl_r0);
bg();
circle(0,0,whl_r1);
fg();
circle(0,0,whl_r2);
for (let i = 0; i < n_spoke; i++){
let a = (i/n_spoke)*PI*2;
ctx.lineWidth = spoke_w;
let r = (whl_r0+whl_r1)/2;
line(0,0,cos(a)*r,sin(a)*r);
}
bg();
circle(0,0,axle_r+whl_sp);
}
function wheel_stub(){
fg();
circle(0,0,whl_r2);
bg();
circle(0,0,axle_r+whl_sp);
}
function axle(){
fg();
ctx.fillRect(-axle_l/2,0,axle_l,axle_w);
slot(-box_l/2-slot_w/2-slot_w-axle_sp,0,axle_w/2,PI/2);
slot( box_l/2+slot_w/2+slot_w+axle_sp,0,axle_w/2,PI/2);
slot(-box_l/2-slot_w/2-slot_w*4-axle_sp,0,axle_w/2,PI/2);
slot( box_l/2+slot_w/2+slot_w*4+axle_sp,0,axle_w/2,PI/2);
slot(0,0,axle_w/2,PI/2);
slot(-frame_sp0,0,axle_w/2,PI/2);
slot(frame_sp0,0,axle_w/2,PI/2);
// ctx.fillRect(-frame_sp0/2-frame_slot_l/2,-slot_w,frame_slot_l,slot_w*2);
// ctx.fillRect( frame_sp0/2-frame_slot_l/2,-slot_w,frame_slot_l,slot_w*2);
teeth(-frame_sp0/2,0,frame_slot_l,-PI/2);
teeth( frame_sp0/2,0,frame_slot_l,-PI/2);
}
function join_axle_whl(){
fg();
circle(0,0,axle_r);
bg();
slot(0,0,axle_w/2,-PI/2,false);
slot(0,0,axle_w/2, PI/2,false);
ctx.save();
ctx.translate(-axle_d/2,0);
slot(0,0,axle_w/2,-PI/2,false);
slot(0,0,axle_w/2,PI/2,false);
ctx.translate(axle_d,0);
slot(0,0,axle_w/2,-PI/2,false);
slot(0,0,axle_w/2,PI/2,false);
ctx.restore();
}
function axle_cap(){
fg();
circle(0,0,axle_cap_r);
bg();
slot(0,axle_w/2,axle_w/2,-PI/2,false);
slot(0,axle_w/2,axle_w/2,PI/2,false);
ctx.save();
ctx.translate(-axle_d/2,0);
slot(0,axle_w/2,axle_w/2,-PI/2,false);
slot(0,axle_w/2,axle_w/2,PI/2,false);
ctx.translate(axle_d,0);
slot(0,axle_w/2,axle_w/2,-PI/2,false);
slot(0,axle_w/2,axle_w/2,PI/2,false);
ctx.restore();
}
function box_bot(){
fg();
ctx.fillRect(-box_l/2,-box_w/2,box_l,box_w);
slot(-frame_sp0/2,0,frame_slot_l/2,PI,false);
slot(-frame_sp0/2,0,frame_slot_l/2,0,false);
slot( frame_sp0/2,0,frame_slot_l/2,PI,false);
slot( frame_sp0/2,0,frame_slot_l/2,0,false);
ctx.save();
ctx.translate(0,-axle_d/2);
slot(-frame_sp0/2,0,frame_slot_l/2,PI,false);
slot(-frame_sp0/2,0,frame_slot_l/2,0,false);
slot( frame_sp0/2,0,frame_slot_l/2,PI,false);
slot( frame_sp0/2,0,frame_slot_l/2,0,false);
ctx.translate(0,axle_d);
slot(-frame_sp0/2,0,frame_slot_l/2,PI,false);
slot(-frame_sp0/2,0,frame_slot_l/2,0,false);
slot( frame_sp0/2,0,frame_slot_l/2,PI,false);
slot( frame_sp0/2,0,frame_slot_l/2,0,false);
ctx.restore();
slot(-frame_sp0,frame_sp1/2,frame_slot_l/2,PI/2,false);
slot(-frame_sp0,frame_sp1/2,frame_slot_l/2,-PI/2,false);
slot(0,frame_sp1/2,frame_slot_l/2,PI/2,false);
slot(0,frame_sp1/2,frame_slot_l/2,-PI/2,false);
slot(frame_sp0,frame_sp1/2,frame_slot_l/2,PI/2,false);
slot(frame_sp0,frame_sp1/2,frame_slot_l/2,-PI/2,false);
slot(-frame_sp0,-frame_sp1/2,frame_slot_l/2,PI/2,false);
slot(-frame_sp0,-frame_sp1/2,frame_slot_l/2,-PI/2,false);
slot(0,-frame_sp1/2,frame_slot_l/2,PI/2,false);
slot(0,-frame_sp1/2,frame_slot_l/2,-PI/2,false);
slot(frame_sp0,-frame_sp1/2,frame_slot_l/2,PI/2,false);
slot(frame_sp0,-frame_sp1/2,frame_slot_l/2,-PI/2,false);
// ctx.fillRect(-box_l/2-slot_w,-frame_sp2/2-frame_slot_l/2,slot_w,frame_slot_l);
// ctx.fillRect(-box_l/2-slot_w, frame_sp2/2-frame_slot_l/2,slot_w,frame_slot_l);
// ctx.fillRect(box_l/2,-frame_sp2/2-frame_slot_l/2,slot_w,frame_slot_l);
// ctx.fillRect(box_l/2, frame_sp2/2-frame_slot_l/2,slot_w,frame_slot_l);
teeth(-box_l/2,-frame_sp2/2,frame_slot_l,PI);
teeth( box_l/2,-frame_sp2/2,frame_slot_l,0);
teeth(-box_l/2, frame_sp2/2,frame_slot_l,PI);
teeth( box_l/2, frame_sp2/2,frame_slot_l,0);
}
function frame_beam(){
fg();
ctx.fillRect(-box_w/2,0,box_w,axle_w);
slot(0,axle_w,axle_w/2,-PI/2);
ctx.save();
ctx.translate(-axle_d/2,0);
slot(0,axle_w,axle_w/2,-PI/2);
ctx.translate(axle_d,0);
slot(0,axle_w,axle_w/2,-PI/2);
ctx.restore();
// ctx.fillRect(-frame_sp1/2-frame_slot_l/2,-slot_w,frame_slot_l,slot_w*2);
// ctx.fillRect( frame_sp1/2-frame_slot_l/2,-slot_w,frame_slot_l,slot_w*2);
teeth(-frame_sp1/2,0,frame_slot_l,-PI/2);
teeth( frame_sp1/2,0,frame_slot_l,-PI/2);
}
function box_side(){
fg();
ctx.fillRect(-box_w/2,-box_h,box_w,box_h+side_bot_p);
ctx.save();
ctx.translate(0,axle_w/2);
slot(0,0,axle_w/2,-PI/2,false);
slot(0,0,axle_w/2, PI/2,false);
ctx.translate(-axle_d/2,0);
slot(0,0,axle_w/2,-PI/2,false);
slot(0,0,axle_w/2,PI/2,false);
ctx.translate(axle_d,0);
slot(0,0,axle_w/2,-PI/2,false);
slot(0,0,axle_w/2,PI/2,false);
ctx.restore();
fg();
circle(-box_w/2+arm_h/3,-box_h,arm_h/3);
ctx.fillRect(-box_w/2+arm_h/3,-box_h-arm_h/3,box_w-arm_h/3,arm_h/3);
circle(box_w/2-side_back_p,-box_h-arm_h/3*2,arm_h/3,PI/2,PI/2*3);
ctx.fillRect(box_w/2-arm_h/3-side_back_p,-box_h-arm_h/3*2,arm_h/3,arm_h);
ctx.fillRect(box_w/2-side_back_p,-box_h-arm_h,side_back_p,arm_h);
// bg();
ctx.beginPath();
ctx.moveTo(box_w/2-arm_h/3-side_back_p,-box_h-arm_h/3);
ctx.arc(box_w/2-arm_h/3*2-side_back_p,-box_h-arm_h/3*2,arm_h/3,0,PI/2);
ctx.fill();
slot(-frame_sp2/2,-slot_w/2,frame_slot_l/2,PI,false);
slot(-frame_sp2/2,-slot_w/2,frame_slot_l/2,0,false);
slot( frame_sp2/2,-slot_w/2,frame_slot_l/2,PI,false);
slot( frame_sp2/2,-slot_w/2,frame_slot_l/2,0,false);
slot(-box_w/2,-box_h+slot_w/2+seat_y,(box_w-side_back_p)/2,0);
slot( box_w/2-side_back_p+slot_w/2,-arm_h-box_h,(arm_h+box_h-slot_w)/2,PI/2);
slot(-box_w/2+handle_x,-box_h+handle_y,handle_slot_l/2,0,false);
slot(-box_w/2+handle_x,-box_h+handle_y,handle_slot_l/2,PI,false);
}
function seat(){
fg();
ctx.fillRect(-box_l/2-axle_sp-slot_w,-box_w/2,box_l+axle_sp*2+slot_w*2,box_w-side_back_p);
slot(-box_l/2-slot_w/2,box_w/2-side_back_p,(box_w-side_back_p)/2,-PI/2);
slot( box_l/2+slot_w/2,box_w/2-side_back_p,(box_w-side_back_p)/2,-PI/2);
teeth(0,box_w/2-side_back_p,seat_teeth_l,PI/2);
}
function seat_back(){
fg();
ctx.fillRect(-box_l/2-axle_sp-slot_w,0,box_l+axle_sp*2+slot_w*2,arm_h+box_h-slot_w);
let w = box_l-pat_p*2;
let h = arm_h+box_h-slot_w-pat_p*2;
let cn0 = pattern(box_l, arm_h+box_h);
ctx.drawImage(cn0,-box_l/2+pat_p,pat_p,w,h);
fg();
// ctx.fillStyle='lime'
ctx.fillRect(-seat_teeth_l/2-pat_p, arm_h+seat_y-pat_p, seat_teeth_l+pat_p*2, slot_w+pat_p*2);
slot(-box_l/2-slot_w/2,arm_h+box_h-slot_w,(arm_h+box_h-slot_w)/2,-PI/2);
slot( box_l/2+slot_w/2,arm_h+box_h-slot_w,(arm_h+box_h-slot_w)/2,-PI/2);
slot(0,arm_h+slot_w/2+seat_y,seat_teeth_l/2,-PI,false);
slot(0,arm_h+slot_w/2+seat_y,seat_teeth_l/2,0,false);
}
function handle(){
ctx.save();
fg();
ctx.lineWidth=handle_w;
ctx.beginPath();
ctx.moveTo(-handle_fl,0);
ctx.lineTo(0,0)
ctx.bezierCurveTo(handle_l/2,0,handle_l/2,-handle_h,handle_l,-handle_h);
ctx.stroke();
ctx.restore();
fg();
ctx.fillRect(-handle_fl,-handle_fw/2,handle_fl,handle_fw);
slot(-handle_fl/2,0,handle_slot_l/2,0,false);
slot(-handle_fl/2,0,handle_slot_l/2,PI,false);
}
function handle_beam(){
fg();
ctx.fillRect(-box_l/2-slot_w*2-handle_bp,0,box_l+slot_w*4+handle_bp*2,handle_slot_l);
slot(-box_l/2-slot_w-slot_w/2,0,handle_slot_l*0.4,PI/2);
slot( box_l/2+slot_w+slot_w/2,0,handle_slot_l*0.4,PI/2);
}
function pattern0(w,h){
let cn0 = createCanvas(w*SCALE,h*SCALE);
let ct0 = cn0.getContext('2d');
ct0.scale(SCALE,SCALE);
ct0.fillStyle = 'black';
ct0.fillRect(0,0,w,h);
ct0.translate(w/2,h/2-10);
ct0.rotate(PI/4);
ct0.strokeStyle = 'white';
ct0.lineWidth = 10;
for (let i = 0; i < 20; i++){
for (let j = 0; j < 20; j++){
ct0.beginPath();
ct0.arc(j*60-20-600,i*60-20-600,40,0,PI*2);
ct0.stroke();
}
}
return cn0;
}
function pattern1(w,h){
let cn0 = createCanvas(w*SCALE,h*SCALE);
let ct0 = cn0.getContext('2d');
ct0.scale(SCALE,SCALE);
ct0.fillStyle = 'black';
ct0.fillRect(0,0,w,h);
ct0.translate(-20,-5);
ct0.strokeStyle = 'white';
ct0.lineWidth = 10;
for (let i = 0; i < 10; i++){
for (let j = 0; j < 10; j++){
ct0.save();
if (i % 2) ct0.translate(-60,0);
ct0.beginPath();
ct0.arc(j*120-40,i*60-40,80,0,PI*2);
ct0.fill();
ct0.stroke();
ct0.beginPath();
ct0.arc(j*120-40,i*60-40,60,0,PI*2);
ct0.fill();
ct0.stroke();
ct0.beginPath();
ct0.arc(j*120-40,i*60-40,40,0,PI*2);
ct0.fill();
ct0.stroke();
ct0.beginPath();
ct0.arc(j*120-40,i*60-40,20,0,PI*2);
ct0.fill();
ct0.stroke();
ct0.restore();
}
}
return cn0;
}
function dist(x0,y0,x1,y1){
return Math.hypot(x0-x1,y0-y1);
}
function lerp(a,b,t){
return a*(1-t)+b*t;
}
function pattern2(w,h){
let cn0 = createCanvas(w,h);
let ct0 = cn0.getContext('2d');
ct0.fillStyle = 'black';
ct0.fillRect(0,0,w,h);
ct0.scale(1.5,1.5);
ct0.strokeStyle = 'white';
let N = 1000;
let N_CAND = 100;
let circs = [];
let dmax = 30;
for (let i = 0; i < N; i++){
let dmin = -Infinity;
let nx, ny;
for (let j = 0; j < N_CAND; j ++){
let x = rand()*w;
let y = rand()*h;
let d = Infinity;
for (let k = 0; k < circs.length; k++){
d = Math.min(d,Math.hypot(circs[k][0] - x,circs[k][1] - y)- circs[k][2]);
}
if (d > dmin){
dmin = d;
nx = x;
ny = y;
}
}
if (dmin < 8){
continue;
}
circs.push([nx,ny,dmin<0?dmax:Math.min(dmin,dmax)]);
}
let nbrs = [];
for (let i = 0; i < circs.length; i++){
let ds = circs.map((x,j)=>[j,dist(circs[i][0],circs[i][1],x[0],x[1])-circs[i][2]-x[2]]);
ds.sort((a,b)=>a[1]-b[1]).shift();
nbrs.push(ds.slice(0,3));
}
// console.log(nbrs);
ct0.fillStyle = 'white';
ct0.lineCap = 'round';
// ct0.filter='blur(3px)';
for (let i = 0; i < circs.length; i++){
// ct0.lineWidth = 1;
// ct0.beginPath();
// ct0.arc(...circs[i],0,PI*2);
// ct0.stroke();
let q = circs[nbrs[i][0][0]];
ct0.lineWidth = 7;
let a0 = Math.atan2(circs[i][1]-q[1],circs[i][0]-q[0])+PI;
ct0.beginPath();
// ct0.moveTo(circs[i][0]+cos(a0)*r0,circs[i][1]+sin(a0)*r0);
for (let j = 0; j < 100; j++){
let t = j/100;
let m = Math.max(1,(circs[i][2]/12));
let a = a0+t*PI*2*m;
let r = (circs[i][2]+6)*(1-t);
let x = circs[i][0] + cos(a)*r;
let y = circs[i][1] + sin(a)*r;
ct0[j?'lineTo':'moveTo'](x,y);
}
ct0.stroke();
}
// let imgdata = ct0.getImageData(0,0,cn0.width,cn0.height)
// for (let i = 0; i < imgdata.data.length; i++){
// imgdata.data[i] = imgdata.data[i] < 128 ? 0 : 255;
// }
// ct0.putImageData(imgdata,0,0);
// console.log('?');
let dat = ct0.getImageData(0,0,cn0.width,cn0.height).data;
let im = [];
for (let i = 0; i < dat.length; i+=4){
im.push(dat[i]>128?255:0);
}
let contours = findContours(im,cn0.width,cn0.height);
// console.log('?');
for (let i = 0; i < contours.length; i++){
contours[i] = approxPolyDP(contours[i].points,1.5);
}
contours = contours.filter(x=>x.length>=3);
// console.log('?');
let cn1 = createCanvas(w*SCALE,h*SCALE);
let ct1 = cn1.getContext('2d');
// ct1.filter='blur(4px)';
ct1.scale(SCALE,SCALE)
ct1.fillStyle = 'black';
ct1.fillRect(0,0,w,h);
ct1.fillStyle = 'white';
ct1.beginPath();
for (let i = 0; i < contours.length; i++){
for (let j = 0; j < contours[i].length; j++){
ct1[j?'lineTo':'moveTo']((contours[i][j][0]-3)*1.1,(contours[i][j][1]-3)*1.1);
}
}
ct1.fill();
// let imgdata = ct1.getImageData(0,0,cn1.width,cn1.height)
// for (let i = 0; i < imgdata.data.length; i++){
// imgdata.data[i] = imgdata.data[i] < 128 ? 0 : 255;
// }
// ct1.putImageData(imgdata,0,0);
// console.log('?');
return cn1;
}
function place(func,x,y,ang=0){
ctx.save();
ctx.translate(x,y);
ctx.rotate(ang);
func();
ctx.restore();
}
function rickshaw(){
cnv.width = W*SCALE;
cnv.height = H*SCALE;
ctx.save();
ctx.fillStyle="black";
ctx.fillRect(0,0,cnv.width,cnv.height);
ctx.scale(SCALE,SCALE);
place(box_side ,1740,990);
place(box_side ,1890,100,PI);
place(wheel ,2190,675);
place(wheel ,1440,250);
place(frame_beam ,2210,920);
place(frame_beam ,2210,1020);
place(frame_beam ,1040,1000,-PI/2);
place(wheel_stub ,2225,130);
place(wheel_stub ,440 ,470);
place(axle_cap ,2320,350);
place(axle_cap ,130 ,120);
place(axle_cap ,1420,1100);
place(axle_cap ,1420,630);
place(axle ,2040,1190,PI);
place(axle ,1180,385, PI/2);
place(axle ,25 ,835,-PI/2);
place(join_axle_whl,1590,530)
place(join_axle_whl,1450,835)
place(join_axle_whl,1200,1130)
place(join_axle_whl,220 ,520)
place(seat_back ,1090 ,495,PI/2)
place(box_bot ,770 ,1000)
place(seat ,320 ,910,-PI/2);
place(handle ,792 ,46,PI);
place(handle ,805 ,125,PI);
place(handle_beam ,1300,750,PI/2);
ctx.restore();
return cnv;
}
function trace(){
let dat = ctx.getImageData(0,0,cnv.width,cnv.height).data;
let im = [];
for (let i = 0; i < dat.length; i+=4){
im.push(dat[i+Number(TRACE_BONE)]>128?255:0);
}
let contours = findContours(im,cnv.width,cnv.height);
for (let i = 0; i < contours.length; i++){
contours[i] = approxPolyDP(contours[i].points,1);
contours[i] = contours[i].map(x=>[x[0]/SCALE,x[1]/SCALE])
}
contours = contours.filter(x=>x.length>=3);
return contours;
}
function draw_svg(polylines){
let o = `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}"><path stroke="black" stroke-width="1" fill="none" d="M0 0 ${W} 0 ${W} ${H} 0 ${H} z"/>`
for (let i = 0; i < polylines.length; i++){
o += `<path stroke="black" stroke-width="1" fill="none" d="M`
for (let j = 0; j < polylines[i].length; j++){
let [x,y] = polylines[i][j];
o += `${x} ${y} `;
}
o += ' z"/>';
}
o += `</svg>`
return o;
}
if (is_node){
rickshaw();
fs.writeFileSync('out.png',cnv.toBuffer('image/png'));
let contours = trace();
let su_rb = `
require 'sketchup.rb'
require 'extensions.rb'
require 'langhandler.rb'
data = ${JSON.stringify(contours)}
depth = ${slot_w}/25.4
def poly_area(poly)
n = poly.length()
a = 0.0
p = n-1
q = 0
loop do
if q >= n then
break
end
a += poly[p][0] * poly[q][1] - poly[q][0] * poly[p][1]
p = q
q = q + 1
end
return a * 0.5
end
data.each_with_index do |pts,i|
puts i
begin
f = Sketchup.active_model.entities.add_face(pts.map{|x| [x[0]/25.4,x[1]/25.4]})
if poly_area(pts) < 0 then
f.pushpull(depth);
else
f.pushpull(-depth);
end
rescue
puts ":P"
end
end
`
fs.writeFileSync('out.svg',draw_svg(contours));
fs.writeFileSync(su_rb_path,su_rb);
}
Cutting
I finished coding the design on Friday, and wanted to load it up in the shop's software and check if it's working at all, before I cut it on Saturday.
We were using VCarve to generate the toolpath; turned out it doesn't take SVG's, so I used Inkscape to convert my output to DXF. VCarve's GUI is quite terrible (I would later learn that ShopBot's is even worse), but with a combination of clicking around and remembering what was taught at the training session, I seemed to have gotten it to work.
The biggest trouble was scaling. People say that with many softwares things are always off by a factor of 25.4 (inch to mm), but for my designs it's always more messed up than that. I think it's got to do with the softwares reading my unit as pixels and assuming some arbitrary DPI. It happened before with CorelDraw and the laser cutter. So I used the same hack: I drew from my code a box of definite dimensions enclosing all the shapes, and in VCarve I made sure that box has the right size.
I also received a personal electric screwdriver training from Tom, since I've never used one before.
On Saturday, I paired up with Reina to cut our designs. We were wondering what would be the best width for our joint slots, so we first cut a joint test Reina made in AutoCAD.
The OSB board was very rough that should you slide your finger even a short distance along the edge, you would surely get splinters. I really like my hands and tend to get depressed when they're damaged, so we immediately put some gloves on.
After cutting out the test joints, I played with the cut pieces a bit and thought the loosest slot (0.46'') worked the best. I could insert it with my bare hands and there seemed to be good friction. But Zach our TA came around and told us the tightest slot (0.425'') is actually the best. He demoed that we can use a mallet to hammer it in.
However my design has a lot of interlocking pieces, so I was worried that if the joints were too tight, it could be very hard to satisfy all the joints at the same time. But Zach reassured me that it was better to file and sand joints that are too tight afterwards, than to have joints that are too loose. That sounded like wise advice at the time, so I set my slot width to 11mm (about 0.43'', since I prefer metric system).
But there was another issue: since my design is so tightly packed and almost bleeding to the borders, we need to perfectly align it with the axes of shopbot, lest the shapes near the edges will be off the board. Moreover, we need to strategically position the screws to hold down the board and could not simply drill a bunch of them along the border. Zach taught us how to solve both issues:
- Move shopbot's arm along the long axis (X) and record its position at either end of the bed. Make marks. Align board to marks.
- After zeroing shopbot, figure out the coordinates of blank areas in my design, and hover shopbot's arm to these places using keypad control. Make marks. Drill screws at marks.
Turned out my preference toward the metric system brought me even more troubles. The first time I tried to cut my design, shopbot merely scraped over the surface of the board. That didn't look right so we stopped the machine and Zach helped take a look. We found that VCarve kept the default numerical values we set previously, but simply changed the units from inches to mm, instead of doing proper length conversion. For example, in the input box for cut depth, the value was still 0.52, which we set during training since we wanted the shopbot to cut 0.52 inches. But the label next to the input box had been automatically changed from “inches” to “mm”, once I imported my file using millimeters. Now the shopbot cut only 0.52mm, hence barely scraping the board. Who expected that to happen?!
After many similarly confusing moments we finally got the machine to work. But it stopped working after cutting just a couple of shapes, with a popup saying that shopbot is no longer recognized. The only option listed was to quit the program and restart everything. I promptly cried out to Zach for help. Zach also found it strange and said it likely had something to do with electricity messing up. Luckily when we restarted the software, shopbot remembered its zeros, so we simply ran the whole design again: it would re-carve the places already carved, but since the machine is accurate the only thing wasted was time.
It turned out that my alignment of the board was almost perfect, except that we set the X/Y zeros a bit too inward, and some of the pieces near the other two edges were almost going off border. But as usual, I was extremely lucky. In the picture below, you can see how the wheel is almost going to be not round, but is still round:
During shop training Tom and John said that a job would only take some 15-20 minutes. Zach, after looking at my design, estimated that it would take 45 minutes. All of them underestimated the nastiness of my design, which actually took more than an hour to complete on the shopbot. The pattern on the seat back was especially slow to carve.
The finished board was a pretty sight to look at, but due to the busy surface texture of the OSB board, its coolness was hard to capture with a photograph.
The next step was to break the tabs and remove the pieces from the board, which we did using hammers and chisels.
The pieces were placed in a corner while we cleaned up the huge mess on and near shopbot using a vacuum. The seat back piece with its patterns looked especially nice, and if the ensuing assembly process was to fail, you'll be reading right now about how I made a pretty window panel for my project. You see, I was very sly, should the wheel system fail I would say I made a pretty chair, and if the chair failed I would point at the seat back and say it's a window, if even that failed I would throw all the pieces together and call it “art”.
Assembly
After helping Reina cut out her foldable coffee-table I started assembling my pieces.
It was orders of magnitude harder than assembling laser-cut cardboard. First, the joints were really tight, so I had to file most of the slots quite a bit. For some of the slots, the other piece is supposed to be stuck at where the slot is, and these joints are easy, no matter how tight it is I can solve with some hammering; but there're also another type of slot in my design, where the other piece is supposed to slide along, and end up some distance away from the slot where it entered. For example, as shown in my wheel system diagram in the first section, the circular disks needs to slide along the axles into their correct position. If you try to solve these with a hammer, the OSB will shatter into pieces before you manage to knock them into position after hours of trying.
While I was miserably filing all the joints, I was wondering if I should not have listened to Zach's advice about making the joints as tight as possible. Then I decided that perhaps I would be in an even worse position if the joints were too loose.
After finishing the chassis, I realized that I couldn't slide in the two side panels. This was because the chassis was fit too tightly together and there was no elasticity to allow slight bending of the axles needed for sliding in the side panels. Unwilling to restart, I tried to hammer it real hard. It ended up breaking a piece from one of the axles. Luckily I designed three parallel axles, so it was merely an aesthetic annoyance rather than a structural disaster.
I partially disassembled the chassis, did a lot of filing, and managed to install the side panels. Then I reinstalled the removed parts of the chassis, and did some hammering all around to make all the joints happy.
The next step was to insert the seat back. I took great care not to hammer it too hard like before: it's the most precious piece of the rickshaw! Gently and slowly I knocked it in.
Reina commented that it looked like a chair fit for a king. I thought that as soon as the king touches his OSB throne and get stabbed by wood splinters all over his hand, he will probably want my head on a platter.
Then I hammered in the seat in a similar fashion. It was not until then that I realized that some of my swirly patterns had actually fallen off, creating circular holes. I looked at earlier photographs, and discovered that these pieces were broken off ever since the shopbot worked on them. Having thrown away the bits as trash, there was apparently no way to recover these swirls. After being sad for a while, I was glad that I decided upon a random pattern: for if I were to use a regular pattern, it would become much more obvious if some parts had fallen off. I was less sad.
Finally came the most exciting part: the wheels. I was very anxious to see if they were gonna roll! I first filed the exterior of the interior disk and the interior of the exterior disk to remove the tabs (in retrospect I shouldn't have placed the tabs there). In my design I gave these disk a 1mm spacing, assuming that this will enable them to rotate against each other without too much friction. I was very unsure at the time if this was the right amount, but it seemed to have ended up being the sweet-spot! The wheel rotated very well, without feeling too loose or too tight. I was lucky again.
However, there was one thing that I overlooked. In my design I left spacing for exactly twice the thickness of the board between the wheel caps (because the rotating part of the wheel has two layers of board, the big wheel and the small ring), but that squeezed the wheel too tightly, and when I hammered in the caps, the wheel no longer rotated smoothly. I needed to make the ring less thick.
Since it didn't sound like a job for the file, I tried to find the toughest and roughest sand paper around the shop. But all the sand papers didn't look tough and rough enough. I wondered if the shop had one of those things that goes spinning really fast, and when you press your thing onto it, it goes vroooom!! and your thing gets sanded already. Turned out there was a machine just like that. Reina showed me the machine and how to use it.
The machine was effective, but it takes a very brave soul to use it: If the object is not thrusted hard enough against it, the object flies away, and you end up grinding your fingers; If you try to thrust your object very hard toward it, you become more prone to accidentally touching it, and still end up grinding your fingers. When I saw how it worked I immediately wanted to protect my hands with gloves, but Reina explained that gloves are not allowed since they might get caught in the machine and cause even greater harm.
So bravely, with my bare hands, I pressed my part by onto the spinning thing. And after grinding my part a couple of times, it became apparently less thick. Then a scary thing happened. I didn't hold the part tightly enough with my right hand, that it swung due to the rotation of the spinning thing, and my left thumb stabbed right into the spinning thing when my left hand tried to help.
My fingernail disappeared! (the small portion that protrudes out of my finger that is). If I've been less swift at retracting my hand, I don't know how much of my finger was going to disappear. As I was too traumatized by the machine, Reina kindly helped me sand the part for my other wheel.
After sanding the parts, the wheels could be spun smoothly. The last step was to install the two shafts. I also used some white glue to attach the small part that was previously broken due to hammering.
By the time I finished assembling the rickshaw, I was extremely hungry and tired. We had been going for ten hours non-stop, from cutting to hammering. I felt like I might have damaged my right arm, since I could barely move it anymore.
On the plus side, I think I've “mastered” the art of hammering. Initially, for each strike, I whacked at the parts with all the strength I can muster. This tends to crack the wood instead of driving it into joints. Through practice, I learned to knock at the parts lightly, but frequently, at different places and from different angles alternatingly. Even though the effect might not be apparent, the wood is actually very slowly being driven in. You have to have patience: even if each knock drives the wood in by a nanometer, with enough knocks you can get a perfectly tight joint without cracking anything.
While working on my project, I also helped my classmates with theirs. I found it slightly amusing that only moments after figuring something out myself, I started teaching it to others. I hope I didn't mislead. I became especially familiar with the crappy software duo: VCarve and ShopBot. In ShopBot, if you change direction on the keypad too quickly, the software freezes and you need to force quit. If you click something other than the keypad without manually closing the keypad first, the software freezes and you need to force quit. And so on. I joked that the software makes you more patient: if you act impatiently it screws you up. But nobody agreed with me: they all thought it makes them more irritated and less patient. I guess it's also a valid point of view.
Now that the rickshaw is made, it's time to test if it works! I drove the rickshaw around the shop for a bit, and after a few hiccups the wheels started to function like real wheels do. It was very satisfying to hear the sound of them working.
But I was more ambitious than that, so now comes the real challenge. I asked Reina to be my passenger, to see if the rickshaw can do what it's supposedly for!
The funny gif below documents the historic moment:
After walking a couple of steps we started to hear ominous sound of squeaking. I tried to drag it some more steps, but the cracking sound only got louder. The rickshaw was not happy! We decided to not push our luck more. Welp, that was a short trip.
I think that the carriage of the rickshaw was actually pretty sturdy. The main issue was with the joints that connect the shaft to the carriage. The shafts are responsible not only for dragging the rickshaw forward, but also for lifting part of the weight put on the carriage. My design overlooked the latter, so the joint type didn't hold well against torque. This is partly due to my stupidity and partly due to the lack of materials: for initially I was designing for robustness, hence the chassis was very sturdy, later I was optimizing for space, when I tried squeeze the shafts into an already packed board.
I thought about how I would design that joint if I had more material. Perhaps I could make two (or even three) of the same joints, vertically spaced, to lock the shaft into place. Or, I could make the side panel and the shaft a whole piece, without joints. But I also suspected that with the reinforced joint, the thin, long shape of the shafts might be the next weakest spot. Theoretically I could make the shafts really, really thick, and only thiner at where the hands are supposed to hold; How to make something like that look pretty is another question. Perhaps using a material other than OSB might a better solution.
Overall I'm still quite happy with the result. Even though my dream of becoming a rickshaw boy was shattered, my handiwork still makes a nice cart to drag stuff around with.
When I took my rickshaw-shaped cart out for a walk around the campus, some random kid kept following me. I think he wanted a ride!