diff --git a/files.html b/files.html
index a0cf98537a41d2d78fb541dfde837e01d164a707..1c1157d1386c0343af3b7b689a2b7758bab76d74 100644
--- a/files.html
+++ b/files.html
@@ -19,7 +19,6 @@
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./js/files.js'>files.js</a><br>
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./js/mods.js'>mods.js</a><br>
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./js/modules.js'>modules.js</a><br>
-<i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;node_modules</i><br>
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./js/printserver.js'>printserver.js</a><br>
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./js/programs.js'>programs.js</a><br>
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./js/serialserver.js'>serialserver.js</a><br>
@@ -192,6 +191,7 @@
 <i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;programs</i><br>
 <i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;frep</i><br>
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./programs/frep/cuboct'>cuboct</a><br>
+&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./programs/frep/gears'>gears</a><br>
 <i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;image</i><br>
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./programs/image/function'>function</a><br>
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./programs/image/motion%20detect'>motion detect</a><br>
diff --git a/programs/frep/gears b/programs/frep/gears
new file mode 100644
index 0000000000000000000000000000000000000000..c032d66872054e02f9f8fe7e7df0e657f437cef2
--- /dev/null
+++ b/programs/frep/gears
@@ -0,0 +1 @@
+{"modules":{"0.1932145895245404":{"definition":"//\n// frep translate\n//\n// Neil Gershenfeld\n// (c) Massachusetts Institute of Technology 2018\n//\n// This work may be reproduced, modified, distributed, performed, and\n// displayed for any purpose, but must acknowledge the mods\n// project. Copyright is retained and must be preserved. The work is\n// provided as is; no warranty is provided, and users accept all\n// liability.\n//\n// closure\n//\n(function(){\n//\n// module globals\n//\nvar mod = {}\n//\n// name\n//\nvar name = 'frep translate'\n//\n// initialization\n//\nvar init = function() {\n   mod.dx.value = '10'\n   mod.dy.value = ''\n   mod.dz.value = ''\n   mod.xmin.value = ''\n   mod.ymin.value = ''\n   mod.zmin.value = ''\n   }\n//\n// inputs\n//\nvar inputs = {\n   shape:{type:'',\n      event:function(evt){\n         mod.shape = evt.detail\n         outputs.shape.event()\n         }},\n   variables:{type:'',\n      event:function(evt){\n         for (var p in evt.detail)\n            mod[p].value = evt.detail[p]\n         outputs.shape.event()\n         }}}\n//\n// outputs\n//\nvar outputs = {\n   shape:{type:'',\n      event:function(){\n         var fn = mod.shape.function\n         var variables = mod.shape.variables\n         var type = mod.shape.type        \n         var limits = []\n         for (var v = 0; v < mod.shape.limits.length; ++v) {\n            limits[v] = []\n            limits[v][0] = mod.shape.limits[v][0]\n            limits[v][1] = mod.shape.limits[v][1]\n            }\n         if (mod.xmin.value != '') {\n            var xmin = parseFloat(mod.xmin.value)\n            var xvar = variables[0]\n            var re = new RegExp(xvar,'g')\n            fn = fn.replace(re,`((${limits[0][0]})+${xvar}-(${xmin}))`)\n            var dx = limits[0][1]-limits[0][0]\n            limits[0][0] = xmin\n            limits[0][1] = xmin+dx\n            }\n         else if (mod.dx.value != '') {\n            var dx = parseFloat(mod.dx.value)\n            var xvar = variables[0]\n            var re = new RegExp(xvar,'g')\n            fn = fn.replace(re,`(${xvar}-(${dx}))`)\n            var dx = parseFloat(mod.dx.value)\n            limits[0][0] += dx\n            limits[0][1] += dx\n            }\n         if (mod.ymin.value != '') {\n            var ymin = parseFloat(mod.ymin.value)\n            var yvar = variables[1]\n            var re = new RegExp(yvar,'g')\n            fn = fn.replace(re,`((${limits[1][0]})+${yvar}-(${ymin}))`)\n            var dy = limits[1][1]-limits[1][0]\n            limits[1][0] = ymin\n            limits[1][1] = ymin+dy\n            }\n         else if (mod.dy.value != '') {\n            var dy = parseFloat(mod.dy.value)\n            var yvar = variables[1]\n            var re = new RegExp(yvar,'g')\n            fn = fn.replace(re,`(${yvar}-(${dy}))`)\n            var dy = parseFloat(mod.dy.value)\n            limits[1][0] += dy\n            limits[1][1] += dy\n            }\n         if (mod.zmin.value != '') {\n            var zmin = parseFloat(mod.zmin.value)\n            var zvar = variables[2]\n            var re = new RegExp(zvar,'g')\n            fn = fn.replace(re,`((${limits[2][0]})+${zvar}-(${zmin}))`)\n            var dz = limits[2][1]-limits[2][0]\n            limits[2][0] = zmin\n            limits[2][1] = zmin+dz\n            }\n         else if (mod.dz.value != '') {\n            var dz = parseFloat(mod.dz.value)\n            var zvar = variables[2]\n            var re = new RegExp(zvar,'g')\n            fn = fn.replace(re,`(${zvar}-(${dz}))`)\n            var dz = parseFloat(mod.dz.value)\n            limits[2][0] += dz\n            limits[2][1] += dz\n            }\n         var shape = {function:fn,variables:variables,limits:limits,type:type}\n         mods.output(mod,'shape',shape)}\n         }}\n//\n// interface\n//\nvar interface = function(div){\n   mod.div = div\n   //\n   // x\n   //\n   div.appendChild(document.createTextNode('xmin: '))\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 6\n      input.addEventListener('input',function(){\n         mod.dx.value = ''\n         })\n      div.appendChild(input)\n      mod.xmin = input\n   div.appendChild(document.createTextNode(' dx: '))\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 6\n      input.addEventListener('input',function(){\n         mod.xmin.value = ''\n         })\n      div.appendChild(input)\n      mod.dx = input\n   div.appendChild(document.createElement('br'))\n   //\n   // y\n   //\n   div.appendChild(document.createTextNode('ymin: '))\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 6\n      input.addEventListener('input',function(){\n         mod.dy.value = ''\n         })\n      div.appendChild(input)\n      mod.ymin = input\n   div.appendChild(document.createTextNode(' dy: '))\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 6\n      input.addEventListener('input',function(){\n         mod.ymin.value = ''\n         })\n      div.appendChild(input)\n      mod.dy = input\n   div.appendChild(document.createElement('br'))\n   //\n   // z\n   //\n   div.appendChild(document.createTextNode('zmin: '))\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 6\n      input.addEventListener('input',function(){\n         mod.dz.value = ''\n         })\n      div.appendChild(input)\n      mod.zmin = input\n   div.appendChild(document.createTextNode(' dz: '))\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 6\n      input.addEventListener('input',function(){\n         mod.zmin.value = ''\n         })\n      div.appendChild(input)\n      mod.dz = input\n   div.appendChild(document.createElement('br'))\n   //\n   // output button\n   //\n   var btn = document.createElement('button')\n      btn.style.padding = mods.ui.padding\n      btn.style.margin = 1\n      btn.appendChild(document.createTextNode('output'))\n      btn.addEventListener('click',function(){\n         outputs.shape.event()\n         })\n      div.appendChild(btn)\n   }\n//\n// local functions\n//\n;\n//\n// return values\n//\nreturn ({\n   mod:mod,\n   name:name,\n   init:init,\n   inputs:inputs,\n   outputs:outputs,\n   interface:interface\n   })\n}())\n","top":"488","left":"778","inputs":{},"outputs":{}},"0.9470390939067861":{"definition":"//\n// frep add\n//\n// Neil Gershenfeld\n// (c) Massachusetts Institute of Technology 2017\n//\n// This work may be reproduced, modified, distributed, performed, and\n// displayed for any purpose, but must acknowledge the mods\n// project. Copyright is retained and must be preserved. The work is\n// provided as is; no warranty is provided, and users accept all\n// liability.\n//\n// closure\n//\n(function(){\n//\n// module globals\n//\nvar mod = {}\n//\n// name\n//\nvar name = 'frep add'\n//\n// initialization\n//\nvar init = function() {\n   mod.shape0 = null\n   mod.shape1 = null\n   mod.fn0.value = ''\n   mod.fn1.value = ''\n   }\n//\n// inputs\n//\nvar inputs = {\n   shape0:{type:'',label:'shape 0',\n      event:function(evt){\n         mod.shape0 = evt.detail\n         mod.fn0.value = evt.detail.function\n         outputs.shape.event()\n         }},\n   shape1:{type:'',label:'shape 1',\n      event:function(evt){\n         mod.shape1 = evt.detail\n         mod.fn1.value = evt.detail.function\n         outputs.shape.event()\n         }},\n   clear:{type:'',\n      event:function(evt){\n         mod.shape0 = null\n         mod.shape1 = null\n         mod.fn0.value = ''\n         mod.fn1.value = ''\n         }}}\n//\n// outputs\n//\nvar outputs = {\n   shape:{type:'',\n      event:function(){\n         if ((mod.shape0 != null) && (mod.shape1 != null)) {\n            var fn = `Math.max(${mod.shape0.function},${mod.shape1.function})`\n            var variables = mod.shape0.variables\n            var type = mod.shape0.type        \n            var limits = []\n            for (var v = 0; v < mod.shape0.limits.length; ++v) {\n               limits[v] = []\n               if (mod.shape0.limits[v][0] < mod.shape1.limits[v][0])\n                  limits[v][0] = mod.shape0.limits[v][0]\n               else\n                  limits[v][0] = mod.shape1.limits[v][0]\n               if (mod.shape0.limits[v][1] > mod.shape1.limits[v][1])\n                  limits[v][1] = mod.shape0.limits[v][1]\n               else\n                  limits[v][1] = mod.shape1.limits[v][1]\n               }\n            var shape = {function:fn,variables:variables,limits:limits,type:type}\n            mods.output(mod,'shape',shape)}\n            }\n         }}\n//\n// interface\n//\nvar interface = function(div){\n   mod.div = div\n   //\n   // function 0\n   //\n   div.appendChild(document.createTextNode('function 0: '))\n   div.appendChild(document.createElement('br'))\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 10\n      div.appendChild(input)\n      mod.fn0 = input\n   div.appendChild(document.createElement('br'))\n   //\n   // function 1\n   //\n   div.appendChild(document.createTextNode('function 1: '))\n   div.appendChild(document.createElement('br'))\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 10\n      div.appendChild(input)\n      mod.fn1 = input\n   div.appendChild(document.createElement('br'))\n   }\n//\n// local functions\n//\n;\n//\n// return values\n//\nreturn ({\n   mod:mod,\n   name:name,\n   init:init,\n   inputs:inputs,\n   outputs:outputs,\n   interface:interface\n   })\n}())\n","top":"179","left":"628","inputs":{},"outputs":{}},"0.5205304092894488":{"definition":"//\n// frep involute gear\n//\n// Neil Gershenfeld\n// (c) Massachusetts Institute of Technology 2018\n//\n// This work may be reproduced, modified, distributed, performed, and\n// displayed for any purpose, but must acknowledge the mods\n// project. Copyright is retained and must be preserved. The work is\n// provided as is; no warranty is provided, and users accept all\n// liability.\n//\n// closure\n//\n(function(){\n//\n// module globals\n//\nvar mod = {}\n//\n// name\n//\nvar name = 'frep involute gear'\n//\n// initialization\n//\nvar init = function() {\n   mod.module.value = '1'\n   mod.angle.value = '20'\n   mod.teeth.value = '10'\n   }\n//\n// inputs\n//\nvar inputs = {\n   variables:{type:'',\n      event:function(evt){\n         for (var p in evt.detail)\n            mod[p].value = evt.detail[p]\n         outputs.variables.event()\n         outputs.shape.event()\n         }}}\n//\n// outputs\n//\nvar outputs = {\n   shape:{type:'',\n      event:function(){\n         var module = parseFloat(mod.module.value)\n         var angle = Math.PI*parseFloat(mod.angle.value)/180\n         var teeth = parseFloat(mod.teeth.value)\n         //\n         // pitch radius\n         //\n         var rp = module*teeth/2 \n         mod.info.innerHTML = `pitch radius: ${rp.toFixed(3)} mm<br>`\n         //\n         // base radius\n         //\n         var rb = rp*Math.cos(angle)\n         mod.info.innerHTML += `base radius: ${rb.toFixed(3)} mm<br>`\n         //\n         // addendum height\n         //\n         var ha = 1.0*module\n         mod.info.innerHTML += `outer radius: ${(rp+ha).toFixed(3)} mm<br>`\n         //\n         // dedendum height\n         //\n         var hd = 1.1*module\n         mod.info.innerHTML += `root radius: ${(rp-hd).toFixed(3)} mm<br>`\n         //\n         // involute angle\n         //\n         var ai = Math.tan(angle)-angle \n         mod.info.innerHTML += `inv angle: ${(ai*180/Math.PI).toFixed(3)} deg<br>`\n         //\n         // pitch angle\n         //\n         var ap = 2*Math.PI/teeth\n         mod.info.innerHTML += `pitch angle: ${(ap*180/Math.PI).toFixed(3)} deg<br>`\n         //\n         // shapes\n         //\n         // must be inside tip\n         //\n         var fn = `${rp}+${ha}-Math.sqrt(X*X+Y*Y)`\n         //\n         // angle must be above bottom involute\n         //\n         var fn = `Math.min(${fn},(Math.PI+Math.atan2(Y,X))%${ap}-(Math.sqrt(Math.pow(Math.max(${rb},Math.sqrt(X*X+Y*Y))/${rb},2)-1)-Math.acos(${rb}/Math.max(${rb},Math.sqrt(X*X+Y*Y)))))`\n         //\n         // angle must be below top involute\n         //\n         var fn = `Math.min(${fn},-(Math.sqrt(Math.pow(Math.max(${rb},Math.sqrt(X*X+Y*Y))/${rb},2)-1)-Math.acos(${rb}/Math.max(${rb},Math.sqrt(X*X+Y*Y))))-(-${ap/2+2*ai}+(Math.PI+Math.atan2(Y,X))%${ap}))`\n         //\n         // root circle\n         //\n         var fn = `Math.max(${fn},${rp-hd}-Math.sqrt(X*X+Y*Y))`\n         //\n         // output\n         //\n         var variables = ['X','Y']\n         var limits = [[-rp-ha,+rp+ha],[-rp-ha,+rp+ha]]\n         var type = 'Magnitude'\n         var shape = {function:fn,variables:variables,limits:limits,type:type}\n         mods.output(mod,'shape',shape)}},\n\n/*\nangle = +-(sqrt((r/rb)^2 - 1) - acos(rb/r)) (r > rb)\n(x % n + n) % n\n*/\n\n   variables:{type:'',\n      event:function(){\n         var module = parseFloat(mod.module.value)\n         var angle = parseFloat(mod.angle.value)\n         var teeth = parseFloat(mod.teeth.value)\n         var vars = {module:module,angle:angle,teeth:teeth}\n         mods.output(mod,'variables',vars)}\n         }}\n//\n// interface\n//\nvar interface = function(div){\n   mod.div = div\n   //\n   // module\n   //\n   div.appendChild(document.createTextNode('module (mm): '))\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 3\n      div.appendChild(input)\n      mod.module = input\n   div.appendChild(document.createElement('br'))\n   //\n   // angle\n   //\n   div.appendChild(document.createTextNode('pressure angle: '))\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 3\n      div.appendChild(input)\n      mod.angle = input\n   div.appendChild(document.createElement('br'))\n   //\n   // teeth \n   //\n   div.appendChild(document.createTextNode('number of teeth: '))\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 3\n      div.appendChild(input)\n      mod.teeth = input\n   div.appendChild(document.createElement('br'))\n   //\n   // info\n   //\n   var info = document.createElement('span')\n      div.appendChild(info)\n      mod.info = info\n   //\n   // output button\n   //\n   var btn = document.createElement('button')\n      btn.style.padding = mods.ui.padding\n      btn.style.margin = 1\n      btn.appendChild(document.createTextNode('output'))\n      btn.addEventListener('click',function(){\n         outputs.variables.event()\n         outputs.shape.event()\n         })\n      div.appendChild(btn)\n   }\n//\n// local functions\n//\n;\n//\n// return values\n//\nreturn ({\n   mod:mod,\n   name:name,\n   init:init,\n   inputs:inputs,\n   outputs:outputs,\n   interface:interface\n   })\n}())\n","top":"66","left":"178","inputs":{},"outputs":{}},"0.11749984994373264":{"definition":"//\n// frep rotate\n//\n// Neil Gershenfeld\n// (c) Massachusetts Institute of Technology 2018\n//\n// This work may be reproduced, modified, distributed, performed, and\n// displayed for any purpose, but must acknowledge the mods\n// project. Copyright is retained and must be preserved. The work is\n// provided as is; no warranty is provided, and users accept all\n// liability.\n//\n// closure\n//\n(function(){\n//\n// module globals\n//\nvar mod = {}\n//\n// name\n//\nvar name = 'frep rotate'\n//\n// initialization\n//\nvar init = function() {\n   mod.rx.value = ''\n   mod.ry.value = ''\n   mod.rz.value = '-1.708'\n   mod.ox.value = '0'\n   mod.oy.value = '0'\n   mod.oz.value = '0'\n   }\n//\n// inputs\n//\nvar inputs = {\n   shape:{type:'',\n      event:function(evt){\n         mod.shape = evt.detail\n         outputs.shape.event()\n         }},\n   variables:{type:'',\n      event:function(evt){\n         for (var p in evt.detail)\n            mod[p].value = evt.detail[p]\n         outputs.shape.event()\n         }}}\n//\n// outputs\n//\nvar outputs = {\n   shape:{type:'',\n      event:function(){\n         var fn = mod.shape.function\n         var variables = mod.shape.variables\n         var type = mod.shape.type        \n         var limits = []\n         for (var v = 0; v < mod.shape.limits.length; ++v) {\n            limits[v] = []\n            limits[v][0] = mod.shape.limits[v][0]\n            limits[v][1] = mod.shape.limits[v][1]\n            }\n         var ox = parseFloat(mod.ox.value)\n         var oy = parseFloat(mod.oy.value)\n         var oz = parseFloat(mod.oz.value)\n         if (mod.rx.value != '') {\n            var rx = parseFloat(mod.rx.value)*Math.PI/180\n            var yvar = variables[1]\n            var zvar = variables[2]\n            var yre = new RegExp(yvar,'g')\n            var zre = new RegExp(zvar,'g')\n            fn = fn.replace(yre,\n               `((${oy})+(${yvar}-(${oy}))*Math.cos(${rx})+(TEMP-(${oz}))*Math.sin(${rx}))`)\n            fn = fn.replace(zre,\n               `((${oz})-(${yvar}-(${oy}))*Math.sin(${rx})+(${zvar}-(${oz}))*Math.cos(${rx}))`)\n            fn = fn.replace(/TEMP/g,zvar)\n            var y0 = limits[1][0]\n            var y1 = limits[1][1]\n            var z0 = limits[2][0]\n            var z1 = limits[2][1]\n            limits[1][0] = Math.min(\n               oy+(y0-oy)*Math.cos(rx)-(z0-oz)*Math.sin(rx),\n               oy+(y0-oy)*Math.cos(rx)-(z1-oz)*Math.sin(rx),\n               oy+(y1-oy)*Math.cos(rx)-(z0-oz)*Math.sin(rx),\n               oy+(y1-oy)*Math.cos(rx)-(z1-oz)*Math.sin(rx))\n            limits[1][1] = Math.max(\n               oy+(y0-oy)*Math.cos(rx)-(z0-oz)*Math.sin(rx),\n               oy+(y0-oy)*Math.cos(rx)-(z1-oz)*Math.sin(rx),\n               oy+(y1-oy)*Math.cos(rx)-(z0-oz)*Math.sin(rx),\n               oy+(y1-oy)*Math.cos(rx)-(z1-oz)*Math.sin(rx))\n            limits[2][0] = Math.min(\n               oz+(y0-oy)*Math.sin(rx)+(z0-oz)*Math.cos(rx),\n               oz+(y0-oy)*Math.sin(rx)+(z1-oz)*Math.cos(rx),\n               oz+(y1-oy)*Math.sin(rx)+(z0-oz)*Math.cos(rx),\n               oz+(y1-oy)*Math.sin(rx)+(z1-oz)*Math.cos(rx))\n            limits[2][1] = Math.max(\n               oz+(y0-oy)*Math.sin(rx)+(z0-oz)*Math.cos(rx),\n               oz+(y0-oy)*Math.sin(rx)+(z1-oz)*Math.cos(rx),\n               oz+(y1-oy)*Math.sin(rx)+(z0-oz)*Math.cos(rx),\n               oz+(y1-oy)*Math.sin(rx)+(z1-oz)*Math.cos(rx))\n            }\n         if (mod.ry.value != '') {\n            var ry = parseFloat(mod.ry.value)*Math.PI/180\n            var xvar = variables[0]\n            var zvar = variables[2]\n            var xre = new RegExp(xvar,'g')\n            var zre = new RegExp(zvar,'g')\n            fn = fn.replace(xre,\n               `((${ox})+(${xvar}-(${ox}))*Math.cos(${ry})+(TEMP-(${oz}))*Math.sin(${ry}))`)\n            fn = fn.replace(zre,\n               `((${oz})-(${xvar}-(${ox}))*Math.sin(${ry})+(${zvar}-(${oz}))*Math.cos(${ry}))`)\n            fn = fn.replace(/TEMP/g,zvar)\n            var x0 = limits[0][0]\n            var x1 = limits[0][1]\n            var z0 = limits[2][0]\n            var z1 = limits[2][1]\n            limits[0][0] = Math.min(\n               ox+(x0-ox)*Math.cos(ry)-(z0-oz)*Math.sin(ry),\n               ox+(x0-ox)*Math.cos(ry)-(z1-oz)*Math.sin(ry),\n               ox+(x1-ox)*Math.cos(ry)-(z0-oz)*Math.sin(ry),\n               ox+(x1-ox)*Math.cos(ry)-(z1-oz)*Math.sin(ry))\n            limits[0][1] = Math.max(\n               ox+(x0-ox)*Math.cos(ry)-(z0-oz)*Math.sin(ry),\n               ox+(x0-ox)*Math.cos(ry)-(z1-oz)*Math.sin(ry),\n               ox+(x1-ox)*Math.cos(ry)-(z0-oz)*Math.sin(ry),\n               ox+(x1-ox)*Math.cos(ry)-(z1-oz)*Math.sin(ry))\n            limits[2][0] = Math.min(\n               oz+(x0-ox)*Math.sin(ry)+(z0-oz)*Math.cos(ry),\n               oz+(x0-ox)*Math.sin(ry)+(z1-oz)*Math.cos(ry),\n               oz+(x1-ox)*Math.sin(ry)+(z0-oz)*Math.cos(ry),\n               oz+(x1-ox)*Math.sin(ry)+(z1-oz)*Math.cos(ry))\n            limits[2][1] = Math.max(\n               oz+(x0-ox)*Math.sin(ry)+(z0-oz)*Math.cos(ry),\n               oz+(x0-ox)*Math.sin(ry)+(z1-oz)*Math.cos(ry),\n               oz+(x1-ox)*Math.sin(ry)+(z0-oz)*Math.cos(ry),\n               oz+(x1-ox)*Math.sin(ry)+(z1-oz)*Math.cos(ry))\n            }\n         if (mod.rz.value != '') {\n            var rz = parseFloat(mod.rz.value)*Math.PI/180\n            var xvar = variables[0]\n            var yvar = variables[1]\n            var xre = new RegExp(xvar,'g')\n            var yre = new RegExp(yvar,'g')\n            fn = fn.replace(xre,\n               `((${ox})+(${xvar}-(${ox}))*Math.cos(${rz})+(TEMP-(${oy}))*Math.sin(${rz}))`)\n            fn = fn.replace(yre,\n               `((${oy})-(${xvar}-(${ox}))*Math.sin(${rz})+(${yvar}-(${oy}))*Math.cos(${rz}))`)\n            fn = fn.replace(/TEMP/g,yvar)\n            var x0 = limits[0][0]\n            var x1 = limits[0][1]\n            var y0 = limits[1][0]\n            var y1 = limits[1][1]\n            limits[0][0] = Math.min(\n               ox+(x0-ox)*Math.cos(rz)-(y0-oy)*Math.sin(rz),\n               ox+(x0-ox)*Math.cos(rz)-(y1-oy)*Math.sin(rz),\n               ox+(x1-ox)*Math.cos(rz)-(y0-oy)*Math.sin(rz),\n               ox+(x1-ox)*Math.cos(rz)-(y1-oy)*Math.sin(rz))\n            limits[0][1] = Math.max(\n               ox+(x0-ox)*Math.cos(rz)-(y0-oy)*Math.sin(rz),\n               ox+(x0-ox)*Math.cos(rz)-(y1-oy)*Math.sin(rz),\n               ox+(x1-ox)*Math.cos(rz)-(y0-oy)*Math.sin(rz),\n               ox+(x1-ox)*Math.cos(rz)-(y1-oy)*Math.sin(rz))\n            limits[1][0] = Math.min(\n               oy+(x0-ox)*Math.sin(rz)+(y0-oy)*Math.cos(rz),\n               oy+(x0-ox)*Math.sin(rz)+(y1-oy)*Math.cos(rz),\n               oy+(x1-ox)*Math.sin(rz)+(y0-oy)*Math.cos(rz),\n               oy+(x1-ox)*Math.sin(rz)+(y1-oy)*Math.cos(rz))\n            limits[1][1] = Math.max(\n               oy+(x0-ox)*Math.sin(rz)+(y0-oy)*Math.cos(rz),\n               oy+(x0-ox)*Math.sin(rz)+(y1-oy)*Math.cos(rz),\n               oy+(x1-ox)*Math.sin(rz)+(y0-oy)*Math.cos(rz),\n               oy+(x1-ox)*Math.sin(rz)+(y1-oy)*Math.cos(rz))\n            }\n         var shape = {function:fn,variables:variables,limits:limits,type:type}\n         mods.output(mod,'shape',shape)}\n         }}\n//\n// interface\n//\nvar interface = function(div){\n   mod.div = div\n   div.appendChild(document.createTextNode('angle\\xa0\\xa0\\xa0 origin'))\n   div.appendChild(document.createElement('br'))\n   //\n   // x\n   //\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 3\n      div.appendChild(input)\n      mod.rx = input\n   div.appendChild(document.createTextNode(' x '))\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 3\n      div.appendChild(input)\n      mod.ox = input\n   div.appendChild(document.createElement('br'))\n   //\n   // y\n   //\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 3\n      div.appendChild(input)\n      mod.ry = input\n   div.appendChild(document.createTextNode(' y '))\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 3\n      div.appendChild(input)\n      mod.oy = input\n   div.appendChild(document.createElement('br'))\n   //\n   // z\n   //\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 3\n      div.appendChild(input)\n      mod.rz = input\n   div.appendChild(document.createTextNode(' z '))\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 3\n      div.appendChild(input)\n      mod.oz = input\n   div.appendChild(document.createElement('br'))\n   //\n   // output button\n   //\n   var btn = document.createElement('button')\n      btn.style.padding = mods.ui.padding\n      btn.style.margin = 1\n      btn.appendChild(document.createTextNode('output'))\n      btn.addEventListener('click',function(){\n         outputs.shape.event()\n         })\n      div.appendChild(btn)\n   }\n//\n// local functions\n//\n;\n//\n// return values\n//\nreturn ({\n   mod:mod,\n   name:name,\n   init:init,\n   inputs:inputs,\n   outputs:outputs,\n   interface:interface\n   })\n}())\n","top":"478","left":"313","inputs":{},"outputs":{}},"0.07937590217387569":{"definition":"//\n// frep view slice\n//\n// Neil Gershenfeld\n// (c) Massachusetts Institute of Technology 2018\n//\n// This work may be reproduced, modified, distributed, performed, and\n// displayed for any purpose, but must acknowledge the mods\n// project. Copyright is retained and must be preserved. The work is\n// provided as is; no warranty is provided, and users accept all\n// liability.\n//\n// closure\n//\n(function(){\n//\n// module globals\n//\nvar mod = {}\n//\n// name\n//\nvar name = 'frep view slice'\n//\n// initialization\n//\nvar init = function() {\n   mod.width.value = '1000'\n   mod.z.value = '0'\n   }\n//\n// inputs\n//\nvar inputs = {\n   shape:{type:'frep',\n      event:function(evt){\n         mod.fn.value = evt.detail.function\n         view_slice(evt.detail)\n         }}}\n//\n// outputs\n//\nvar outputs = {\n   image:{type:'RGBA',\n      event:function(){\n         var ctx = mod.img.getContext(\"2d\")\n         var img = ctx.getImageData(0,0,mod.img.width,mod.img.height)\n         mods.output(mod,'image',img)\n         }}}\n//\n// interface\n//\nvar interface = function(div){\n   mod.div = div\n   //\n   // on-screen drawing canvas\n   //\n   var canvas = document.createElement('canvas')\n      canvas.width = mods.ui.canvas\n      canvas.height = mods.ui.canvas\n      canvas.style.backgroundColor = 'rgb(255,255,255)'\n      div.appendChild(canvas)\n      mod.canvas = canvas\n   div.appendChild(document.createElement('br'))\n   //\n   // off-screen image canvas\n   //\n   var canvas = document.createElement('canvas')\n      mod.img = canvas\n   //\n   // x\n   //\n   div.appendChild(document.createTextNode('x: '))\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 3\n      input.addEventListener('input',function(){\n         mod.y.value = ''\n         mod.z.value = ''\n         })\n      div.appendChild(input)\n      mod.x = input\n   //\n   // y\n   //\n   div.appendChild(document.createTextNode(' y: '))\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 3\n      input.addEventListener('input',function(){\n         mod.x.value = ''\n         mod.z.value = ''\n         })\n      div.appendChild(input)\n      mod.y = input\n   //\n   // z\n   //\n   div.appendChild(document.createTextNode(' z: '))\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 3\n      input.addEventListener('input',function(){\n         mod.x.value = ''\n         mod.y.value = ''\n         })\n      div.appendChild(input)\n      mod.z = input\n   div.appendChild(document.createElement('br'))\n   //\n   // width\n   //\n   div.appendChild(document.createTextNode('width: '))\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 3\n      input.addEventListener('input',function(){\n         mod.height.value = ''\n         })\n      div.appendChild(input)\n      mod.width = input\n   //\n   // height\n   //\n   div.appendChild(document.createTextNode(' height: '))\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 3\n      input.addEventListener('input',function(){\n         mod.width.value = ''\n         })\n      div.appendChild(input)\n      mod.height = input\n   div.appendChild(document.createElement('br'))\n   //\n   // function\n   //\n   div.appendChild(document.createTextNode('function: '))\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 10\n      div.appendChild(input)\n      mod.fn = input\n   div.appendChild(document.createElement('br'))\n   //\n   // view button\n   //\n   div.appendChild(document.createTextNode(' '))\n   var btn = document.createElement('button')\n      btn.style.padding = mods.ui.padding\n      btn.style.margin = 1\n      btn.appendChild(document.createTextNode('view'))\n      btn.addEventListener('click',function(){\n         mod.win = window.open('')\n         mod.win.document.body.style.overflow = 'hidden'\n         mod.win.document.body.style.border = 0\n         mod.win.document.body.style.padding = 0\n         mod.win.document.body.style.margin = 0\n         mod.win.addEventListener('unload',function() {\n            mod.win = null\n            })\n         var canvas = document.createElement('canvas')\n            canvas.setAttribute('id',mod.div.id+'canvas')\n            mod.win.document.body.appendChild(canvas)\n         var ctx = canvas.getContext(\"2d\")\n         canvas.width = window.innerWidth\n         canvas.height = window.innerWidth\n         var h = mod.img.height\n         var w = mod.img.width\n         if (w > h) {\n            var wd = canvas.width\n            var hd = canvas.width*h/w\n            }\n         else {\n            var wd = canvas.height*w/h\n            var hd = canvas.height\n            }\n         ctx.fillStyle = '#dddddd'\n         ctx.fillRect(0,0,canvas.width,canvas.height)\n         ctx.drawImage(mod.img,0,0,wd,hd)\n         })\n      div.appendChild(btn)\n   }\n//\n// local functions\n//\n// view_slice\n//\nfunction view_slice(shape) {   \n   var fn = shape.function\n   var vars = shape.variables\n   var limits = shape.limits\n   var type = shape.type\n   var x = parseFloat(mod.x.value)\n   var y = parseFloat(mod.y.value)\n   var z = parseFloat(mod.z.value)\n   var w = parseInt(mod.width.value)\n   var h = parseInt(mod.height.value)\n   if (vars.length == 2) {\n      var xvar = vars[0]\n      var xlimits = limits[0]\n      var yvar = vars[1]\n      var ylimits = limits[1]\n      var zvar = ''\n      var zval = 0\n      }\n   else if (!isNaN(x)) {\n      var xvar = vars[1]\n      var xlimits = limits[1]\n      var yvar = vars[2]\n      var ylimits = limits[2]\n      var zvar = vars[0]\n      var zval = x\n      }\n   else if (!isNaN(y)) {\n      var xvar = vars[0]\n      var xlimits = limits[0]\n      var yvar = vars[2]\n      var ylimits = limits[2]\n      var zvar = vars[1]\n      var zval = y\n      }\n   else if (!isNaN(z)) {\n      var xvar = vars[0]\n      var xlimits = limits[0]\n      var yvar = vars[1]\n      var ylimits = limits[1]\n      var zvar = vars[2]\n      var zval = z\n      }\n   if (!isNaN(w))\n      h = Math.floor(w*(ylimits[1]-ylimits[0])/(xlimits[1]-xlimits[0]))\n   else\n      w = Math.floor(h*(xlimits[1]-xlimits[0])/(ylimits[1]-ylimits[0]))\n   mod.img.height = h\n   mod.img.width = w\n   var blob = new Blob(['('+worker.toString()+'())'])\n   var url = window.URL.createObjectURL(blob)\n   var webworker = new Worker(url)\n   webworker.addEventListener('message',function(evt) {\n      window.URL.revokeObjectURL(url)\n      var h = mod.img.height\n      var w = mod.img.width\n      var buf = new Uint8ClampedArray(evt.data.buffer)\n      var imgdata = new ImageData(buf,w,h)\n      var ctx = mod.img.getContext(\"2d\")\n      ctx.putImageData(imgdata,0,0)\n      if (w > h) {\n         var x0 = 0\n         var y0 = mod.canvas.height*.5*(1-h/w)\n         var wd = mod.canvas.width\n         var hd = mod.canvas.width*h/w\n         }\n      else {\n         var x0 = mod.canvas.width*.5*(1-w/h)\n         var y0 = 0\n         var wd = mod.canvas.height*w/h\n         var hd = mod.canvas.height\n         }\n      var ctx = mod.canvas.getContext(\"2d\")\n      ctx.fillStyle = '#dddddd'\n      ctx.fillRect(0,0,mod.canvas.width,mod.canvas.height)\n      ctx.drawImage(mod.img,x0,y0,wd,hd)\n      if (mod.win != null) {\n         var canvas = mod.win.document.getElementById(mod.div.id+'canvas')\n         var ctx = canvas.getContext(\"2d\")\n         canvas.width = window.innerWidth\n         canvas.height = window.innerWidth\n         var h = mod.img.height\n         var w = mod.img.width\n         if (w > h) {\n            var wd = canvas.width\n            var hd = canvas.width*h/w\n            }\n         else {\n            var wd = canvas.height*w/h\n            var hd = canvas.height\n            }\n         ctx.fillStyle = '#dddddd'\n         ctx.fillRect(0,0,canvas.width,canvas.height)\n         ctx.drawImage(mod.img,0,0,wd,hd)\n         }\n      webworker.terminate()\n      outputs.image.event()\n      })\n   var ctx = mod.canvas.getContext(\"2d\")\n   ctx.fillStyle = '#dddddd'\n   ctx.fillRect(0,0,mod.canvas.width,mod.canvas.height)\n   var ctx = mod.img.getContext(\"2d\")\n   ctx.fillStyle = '#dddddd'\n   ctx.fillRect(0,0,mod.img.width,mod.img.height)\n   var img = ctx.getImageData(0,0,mod.img.width,mod.img.height)\n   webworker.postMessage({\n      height:img.height,width:img.width,\n      xvar:xvar,yvar:yvar,zvar:zvar,zval:zval,\n      xlimits:xlimits,ylimits:ylimits,\n      function:fn,\n      buffer:img.data.buffer},\n      [img.data.buffer])\n   }\n//\n// worker\n//\nfunction worker() {\n   self.addEventListener('message',function(evt) {\n      var h = evt.data.height\n      var w = evt.data.width\n      var xvar = evt.data.xvar\n      var yvar = evt.data.yvar\n      var zvar = evt.data.zvar\n      var zval = evt.data.zval\n      var xlimits = evt.data.xlimits\n      var ylimits = evt.data.ylimits\n      var buf = new Uint8ClampedArray(evt.data.buffer)\n      var f = new Function(xvar,yvar,zvar,'return ('+evt.data.function+')')\n      var x,y,v\n      for (var row = 0; row < h; ++row) {\n         y = ylimits[0]+(ylimits[1]-ylimits[0])*row/(h-1)\n         for (var col = 0; col < w; ++col) {\n            x = xlimits[0]+(xlimits[1]-xlimits[0])*col/(w-1)\n            v = (f(x,y,zval) >= 0) ? 255 : 0\n            buf[(h-1-row)*w*4+col*4+0] = v\n            buf[(h-1-row)*w*4+col*4+1] = v\n            buf[(h-1-row)*w*4+col*4+2] = v\n            buf[(h-1-row)*w*4+col*4+3] = 255\n            }\n         }\n      self.postMessage({buffer:buf.buffer},[buf.buffer])\n      })\n   }\n//\n// return values\n//\nreturn ({\n   mod:mod,\n   name:name,\n   init:init,\n   inputs:inputs,\n   outputs:outputs,\n   interface:interface\n   })\n}())\n","top":"48","left":"1063","inputs":{},"outputs":{}}},"links":["{\"source\":\"{\\\"id\\\":\\\"0.1932145895245404\\\",\\\"type\\\":\\\"outputs\\\",\\\"name\\\":\\\"shape\\\"}\",\"dest\":\"{\\\"id\\\":\\\"0.9470390939067861\\\",\\\"type\\\":\\\"inputs\\\",\\\"name\\\":\\\"shape1\\\"}\"}","{\"source\":\"{\\\"id\\\":\\\"0.5205304092894488\\\",\\\"type\\\":\\\"outputs\\\",\\\"name\\\":\\\"shape\\\"}\",\"dest\":\"{\\\"id\\\":\\\"0.11749984994373264\\\",\\\"type\\\":\\\"inputs\\\",\\\"name\\\":\\\"shape\\\"}\"}","{\"source\":\"{\\\"id\\\":\\\"0.11749984994373264\\\",\\\"type\\\":\\\"outputs\\\",\\\"name\\\":\\\"shape\\\"}\",\"dest\":\"{\\\"id\\\":\\\"0.1932145895245404\\\",\\\"type\\\":\\\"inputs\\\",\\\"name\\\":\\\"shape\\\"}\"}","{\"source\":\"{\\\"id\\\":\\\"0.9470390939067861\\\",\\\"type\\\":\\\"outputs\\\",\\\"name\\\":\\\"shape\\\"}\",\"dest\":\"{\\\"id\\\":\\\"0.07937590217387569\\\",\\\"type\\\":\\\"inputs\\\",\\\"name\\\":\\\"shape\\\"}\"}","{\"source\":\"{\\\"id\\\":\\\"0.9470390939067861\\\",\\\"type\\\":\\\"outputs\\\",\\\"name\\\":\\\"shape\\\"}\",\"dest\":\"{\\\"id\\\":\\\"0.9470390939067861\\\",\\\"type\\\":\\\"inputs\\\",\\\"name\\\":\\\"clear\\\"}\"}","{\"source\":\"{\\\"id\\\":\\\"0.5205304092894488\\\",\\\"type\\\":\\\"outputs\\\",\\\"name\\\":\\\"shape\\\"}\",\"dest\":\"{\\\"id\\\":\\\"0.9470390939067861\\\",\\\"type\\\":\\\"inputs\\\",\\\"name\\\":\\\"shape0\\\"}\"}"]}
\ No newline at end of file
diff --git a/programs/index.js b/programs/index.js
index b1872d4d5d32a0d720744d1861b2d2551f95c90f..525d515adae5f4c72f46dabdfbe46d2d01322687 100644
--- a/programs/index.js
+++ b/programs/index.js
@@ -1,5 +1,6 @@
 program_label('frep')
 program_menu('   cuboct','programs/frep/cuboct')
+program_menu('   gears','programs/frep/gears')
 program_label('image')
 program_menu('   function','programs/image/function')
 program_menu('   motion detect','programs/image/motion%20detect')