diff --git a/files.html b/files.html
index ed1a71bf25bb68a68b2a6ff8e85ee14e240343cb..1f90fd01e0cb4ce6e48287dfb2f0f139d3dce24e 100644
--- a/files.html
+++ b/files.html
@@ -252,6 +252,7 @@
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./programs/machines/ShopBot/mill%202D%20stl'>mill 2D stl</a><br>
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./programs/machines/ShopBot/mill%202D%20svg'>mill 2D svg</a><br>
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./programs/machines/ShopBot/mill%202D%20svg%20connect'>mill 2D svg connect</a><br>
+&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./programs/machines/ShopBot/mill%203D%20stl'>mill 3D stl</a><br>
 <i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Trotec</i><br>
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='./programs/machines/Trotec/cut%20png'>cut png</a><br>
 <i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;math</i><br>
diff --git a/modules/processes/mill/raster/3D b/modules/processes/mill/raster/3D
index ca76f580699bf1f2e46903dc1dbed9f67c3272f1..f04a57fcdaab82bcfbd6415b7197459d7da8da38 100644
--- a/modules/processes/mill/raster/3D
+++ b/modules/processes/mill/raster/3D
@@ -131,7 +131,7 @@ var interface = function(div){
    //
    // fit error 
    //
-   div.appendChild(document.createTextNode('vector fit (pixels): '))
+   div.appendChild(document.createTextNode('vector fit (%): '))
    //div.appendChild(document.createElement('br'))
    var input = document.createElement('input')
       input.type = 'text'
diff --git a/programs/index.js b/programs/index.js
index fd8318b84119b694f387ba814f605580d2ada813..33049326328d33661c9a2afec129523cf9f17fb6 100644
--- a/programs/index.js
+++ b/programs/index.js
@@ -47,6 +47,7 @@ program_menu('      mill 2D png PCB','programs/machines/ShopBot/mill%202D%
 program_menu('      mill 2D stl','programs/machines/ShopBot/mill%202D%20stl')
 program_menu('      mill 2D svg','programs/machines/ShopBot/mill%202D%20svg')
 program_menu('      mill 2D svg connect','programs/machines/ShopBot/mill%202D%20svg%20connect')
+program_menu('      mill 3D stl','programs/machines/ShopBot/mill%203D%20stl')
 program_label('   Trotec')
 program_menu('      cut png','programs/machines/Trotec/cut%20png')
 program_label('math')
diff --git a/programs/machines/ShopBot/mill 3D stl b/programs/machines/ShopBot/mill 3D stl
new file mode 100644
index 0000000000000000000000000000000000000000..2a1f09f09e23082a324a837faaa30ee23df929d1
--- /dev/null
+++ b/programs/machines/ShopBot/mill 3D stl	
@@ -0,0 +1 @@
+{"modules":{"0.9903638182304415":{"definition":"//\n// read stl\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 = 'read STL'\n//\n// initialization\n//\nvar init = function() {\n   }\n//\n// inputs\n//\nvar inputs = {\n   }\n//\n// outputs\n//\nvar outputs = {\n   mesh:{type:'STL',\n      event:function(buffer){\n         mods.output(mod,'mesh',buffer)}}\n      }\n//\n// interface\n//\nvar interface = function(div){\n   mod.div = div\n   //\n   // file input control\n   //\n   var file = document.createElement('input')\n      file.setAttribute('type','file')\n      file.setAttribute('id',div.id+'file_input')\n      file.style.position = 'absolute'\n      file.style.left = 0\n      file.style.top = 0\n      file.style.width = 0\n      file.style.height = 0\n      file.style.opacity = 0\n      file.addEventListener('change',function() {\n         stl_read_handler()\n         })\n      div.appendChild(file)\n      mod.file = file\n   //\n   // 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   // file select 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('select stl file'))\n      btn.addEventListener('click',function(){\n         var file = document.getElementById(div.id+'file_input')\n         file.value = null\n         file.click()\n         })\n      div.appendChild(btn)\n   div.appendChild(document.createElement('br'))\n   //\n   // info\n   //\n   var info = document.createElement('div')\n      info.setAttribute('id',div.id+'info')\n      var text = document.createTextNode('name: ')\n         info.appendChild(text)\n         mod.namen = text\n      info.appendChild(document.createElement('br'))\n      var text = document.createTextNode('size: ')\n         info.appendChild(text)\n         mod.sizen = text\n      info.appendChild(document.createElement('br'))\n      var text = document.createTextNode('triangles: ')\n         info.appendChild(text)\n         mod.trianglesn = text\n      info.appendChild(document.createElement('br'))\n      var text = document.createTextNode('dx: ')\n         info.appendChild(text)\n         mod.dxn = text\n      info.appendChild(document.createElement('br'))\n      var text = document.createTextNode('dy: ')\n         info.appendChild(text)\n         mod.dyn = text\n      info.appendChild(document.createElement('br'))\n      var text = document.createTextNode('dz: ')\n         info.appendChild(text)\n         mod.dzn = text\n      div.appendChild(info)\n   }\n//\n// local functions\n//\n// read handler\n//\nfunction stl_read_handler(event) {\n   var file_reader = new FileReader()\n   file_reader.onload = stl_load_handler\n   input_file = mod.file.files[0]\n   file_name = input_file.name\n   mod.namen.nodeValue = 'name: '+file_name\n   file_reader.readAsArrayBuffer(input_file)\n   }\n//\n// load handler\n//\nfunction stl_load_handler(event) {\n   //\n   // check for binary STL\n   //\n   var endian = true\n   var view = new DataView(event.target.result)\n   var triangles = view.getUint32(80,endian)\n   var size = 80+4+triangles*(4*12+2)\n   if (size != view.byteLength) {\n      mod.sizen.nodeValue = 'error: not binary STL'\n      mod.trianglesn.nodeValue = ''\n      mod.dxn.nodeValue = ''\n      mod.dyn.nodeValue = ''\n      mod.dzn.nodeValue = ''\n      return\n      }\n   mod.sizen.nodeValue = 'size: '+size\n   mod.trianglesn.nodeValue = 'triangles: '+triangles\n   //\n   // find limits and draw\n   //\n   var blob = new Blob(['('+draw_limits_worker.toString()+'())'])\n   var url = window.URL.createObjectURL(blob)\n   var webworker = new Worker(url)\n   webworker.addEventListener('message',function(evt) {\n      //\n      // worker response\n      //\n      window.URL.revokeObjectURL(url)\n      //\n      // size\n      //\n      mod.dxn.nodeValue = 'dx: '+evt.data.dx.toFixed(3)\n      mod.dyn.nodeValue = 'dy: '+evt.data.dy.toFixed(3)\n      mod.dzn.nodeValue = 'dz: '+evt.data.dz.toFixed(3)\n      //\n      // image\n      //\n      var image = evt.data.image\n      var height = mod.canvas.height\n      var width = mod.canvas.width\n      var buffer = new Uint8ClampedArray(evt.data.image)\n      var imgdata = new ImageData(buffer,width,height)\n      var ctx = mod.canvas.getContext(\"2d\")\n      ctx.putImageData(imgdata,0,0)\n      //\n      // output\n      //\n      outputs.mesh.event(evt.data.mesh)\n      })\n   var ctx = mod.canvas.getContext(\"2d\")\n   ctx.clearRect(0,0,mod.canvas.width,mod.canvas.height)\n   var img = ctx.getImageData(0,0,mod.canvas.width,mod.canvas.height)\n   //\n   // call worker\n   //\n   webworker.postMessage({\n      height:mod.canvas.height,width:mod.canvas.width,\n      image:img.data.buffer,mesh:event.target.result},\n      [img.data.buffer,event.target.result])\n   }\nfunction draw_limits_worker() {\n   self.addEventListener('message',function(evt) {\n      //\n      // function to draw line\n      //\n      function line(x0,y0,x1,y1) {\n         var ix0 = Math.floor(xo+xw*(x0-xmin)/dx)\n         var iy0 = Math.floor(yo+yh*(ymax-y0)/dy)\n         var ix1 = Math.floor(xo+xw*(x1-xmin)/dx)\n         var iy1 = Math.floor(yo+yh*(ymax-y1)/dy)\n         var row,col\n         var idx = ix1-ix0\n         var idy = iy1-iy0\n         if (Math.abs(idy) > Math.abs(idx)) {\n            (idy > 0) ?\n               (row0=iy0,col0=ix0,row1=iy1,col1=ix1):\n               (row0=iy1,col0=ix1,row1=iy0,col1=ix0)\n            for (row = row0; row <= row1; ++row) {\n               col = Math.floor(col0+(col1-col0)*(row-row0)/(row1-row0))\n               image[row*width*4+col*4+0] = 0\n               image[row*width*4+col*4+1] = 0\n               image[row*width*4+col*4+2] = 0\n               image[row*width*4+col*4+3] = 255\n               }\n            }\n         else if ((Math.abs(idx) >= Math.abs(idy)) && (idx != 0)) {\n            (idx > 0) ?\n               (row0=iy0,col0=ix0,row1=iy1,col1=ix1):\n               (row0=iy1,col0=ix1,row1=iy0,col1=ix0)\n            for (col = col0; col <= col1; ++col) {\n               row = Math.floor(row0+(row1-row0)*(col-col0)/(col1-col0))\n               image[row*width*4+col*4+0] = 0\n               image[row*width*4+col*4+1] = 0\n               image[row*width*4+col*4+2] = 0\n               image[row*width*4+col*4+3] = 255\n               }\n            }\n         else {\n            row = iy0\n            col = ix0\n            image[row*width*4+col*4+0] = 0\n            image[row*width*4+col*4+1] = 0\n            image[row*width*4+col*4+2] = 0\n            image[row*width*4+col*4+3] = 255\n            }\n         }\n      //\n      // get variables\n      //\n      var height = evt.data.height\n      var width = evt.data.width\n      var endian = true\n      var image = new Uint8ClampedArray(evt.data.image)\n      var view = new DataView(evt.data.mesh)\n      var triangles = view.getUint32(80,endian)\n      //\n      // find limits\n      //\n      var offset = 80+4\n      var x0,x1,x2,y0,y1,y2,z0,z1,z2\n      var xmin = Number.MAX_VALUE\n      var xmax = -Number.MAX_VALUE\n      var ymin = Number.MAX_VALUE\n      var ymax = -Number.MAX_VALUE\n      var zmin = Number.MAX_VALUE\n      var zmax = -Number.MAX_VALUE\n      for (var t = 0; t < triangles; ++t) {\n         offset += 3*4\n         x0 = view.getFloat32(offset,endian)\n         offset += 4\n         if (x0 > xmax) xmax = x0\n         if (x0 < xmin) xmin = x0\n         y0 = view.getFloat32(offset,endian)\n         offset += 4\n         if (y0 > ymax) ymax = y0\n         if (y0 < ymin) ymin = y0\n         z0 = view.getFloat32(offset,endian)\n         offset += 4\n         if (z0 > zmax) zmax = z0\n         if (z0 < zmin) zmin = z0\n         x1 = view.getFloat32(offset,endian)\n         offset += 4\n         if (x1 > xmax) xmax = x1\n         if (x1 < xmin) xmin = x1\n         y1 = view.getFloat32(offset,endian)\n         offset += 4\n         if (y1 > ymax) ymax = y1\n         if (y1 < ymin) ymin = y1\n         z1 = view.getFloat32(offset,endian)\n         offset += 4\n         if (z1 > zmax) zmax = z1\n         if (z1 < zmin) zmin = z1\n         x2 = view.getFloat32(offset,endian)\n         offset += 4\n         if (x2 > xmax) xmax = x2\n         if (x2 < xmin) xmin = x2\n         y2 = view.getFloat32(offset,endian)\n         offset += 4\n         if (y2 > ymax) ymax = y2\n         if (y2 < ymin) ymin = y2\n         z2 = view.getFloat32(offset,endian)\n         offset += 4\n         if (z2 > zmax) zmax = z2\n         if (z2 < zmin) zmin = z2\n         offset += 2\n         }\n      var dx = xmax-xmin\n      var dy = ymax-ymin\n      var dz = zmax-zmin\n      //\n      // draw mesh\n      //\n      if (dx > dy) {\n         var xo = 0\n         var yo = height*.5*(1-dy/dx)\n         var xw = width-1\n         var yh = (width-1)*dy/dx\n         }\n      else {\n         var xo = width*.5*(1-dx/dy)\n         var yo = 0\n         var xw = (height-1)*dx/dy\n         var yh = height-1\n         }\n      offset = 80+4\n      for (var t = 0; t < triangles; ++t) {\n         offset += 3*4\n         x0 = view.getFloat32(offset,endian)\n         offset += 4\n         y0 = view.getFloat32(offset,endian)\n         offset += 4\n         z0 = view.getFloat32(offset,endian)\n         offset += 4\n         x1 = view.getFloat32(offset,endian)\n         offset += 4\n         y1 = view.getFloat32(offset,endian)\n         offset += 4\n         z1 = view.getFloat32(offset,endian)\n         offset += 4\n         x2 = view.getFloat32(offset,endian)\n         offset += 4\n         y2 = view.getFloat32(offset,endian)\n         offset += 4\n         z2 = view.getFloat32(offset,endian)\n         offset += 4\n         offset += 2\n         line(x0,y0,x1,y1)\n         line(x1,y1,x2,y2)\n         line(x2,y2,x0,y0)\n         }\n      //\n      // return results and close\n      //\n      self.postMessage({\n         dx:dx,dy:dy,dz:dz,\n         image:evt.data.image,mesh:evt.data.mesh},[evt.data.image,evt.data.mesh])\n      self.close()\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":"102.61657499836218","left":"97.22330127332333","inputs":{},"outputs":{}},"0.7269098240824425":{"definition":"//\n// mesh height map\n// \n// Neil Gershenfeld 1/16/20\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 = 'mesh height map'\n//\n// initialization\n//\nvar init = function() {\n   mod.mmunits.value = '25.4'\n   mod.inunits.value = '1'\n   mod.width.value = '500'\n   mod.border.value = '0'\n   }\n//\n// inputs\n//\nvar inputs = {\n   mesh:{type:'STL',\n      event:function(evt){\n         mod.mesh = new DataView(evt.detail)\n         find_limits_map()}}}\n//\n// outputs\n//\nvar outputs = {\n   map:{type:'',label:'height map',\n      event:function(heightmap){\n         var obj = {}\n         obj.map = heightmap\n         obj.xmin = mod.xmin\n         obj.xmax = mod.xmax\n         obj.ymin = mod.ymin\n         obj.ymax = mod.ymax\n         obj.zmin = mod.zmin\n         obj.zmax = mod.zmax\n         obj.width = mod.img.width\n         obj.height = mod.img.height\n         mods.output(mod,'map',obj)\n         }}}\n//\n// interface\n//\nvar interface = function(div){\n   mod.div = div\n   //\n   // on-screen height map canvas\n   //\n   div.appendChild(document.createTextNode(' '))\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.mapcanvas = 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   // mesh units\n   //\n   div.appendChild(document.createTextNode('mesh units: (enter)'))\n   div.appendChild(document.createElement('br'))\n   div.appendChild(document.createTextNode('mm: '))\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 6\n      input.addEventListener('change',function(){\n         mod.inunits.value = parseFloat(mod.mmunits.value)/25.4\n         find_limits_map()\n         })\n      div.appendChild(input)\n      mod.mmunits = input\n   div.appendChild(document.createTextNode(' in: '))\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 6\n      input.addEventListener('change',function(){\n         mod.mmunits.value = parseFloat(mod.inunits.value)*25.4\n         find_limits_map()\n         })\n      div.appendChild(input)\n      mod.inunits = input\n   //\n   // mesh size\n   //\n   div.appendChild(document.createElement('br'))\n   div.appendChild(document.createTextNode('mesh size:'))\n   div.appendChild(document.createElement('br'))\n   var text = document.createTextNode('XxYxZ (units)')\n      div.appendChild(text)\n      mod.meshsize = text\n   div.appendChild(document.createElement('br'))\n   var text = document.createTextNode('XxYxZ (mm)')\n      div.appendChild(text)\n      mod.mmsize = text\n   div.appendChild(document.createElement('br'))\n   var text = document.createTextNode('XxYxZ (in)')\n      div.appendChild(text)\n      mod.insize = text\n   //\n   // height map border \n   //\n   div.appendChild(document.createElement('br'))\n   div.appendChild(document.createTextNode('border: '))\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 6\n      input.addEventListener('change',function(){\n         find_limits_map()\n         })\n      div.appendChild(input)\n      mod.border = input\n   div.appendChild(document.createTextNode(' (units)'))\n   //\n   // height map width\n   //\n   div.appendChild(document.createElement('br'))\n   div.appendChild(document.createTextNode('width: '))\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 6\n      input.addEventListener('change',function(){\n         find_limits_map()\n         })\n      div.appendChild(input)\n      mod.width = input\n   div.appendChild(document.createTextNode(' (pixels)'))\n   //\n   // view height map\n   //\n   div.appendChild(document.createElement('br'))\n   var btn = document.createElement('button')\n      btn.style.padding = mods.ui.padding\n      btn.style.margin = 1\n      btn.appendChild(document.createTextNode('view height map'))\n      btn.addEventListener('click',function(){\n         var win = window.open('')\n         var btn = document.createElement('button')\n            btn.appendChild(document.createTextNode('close'))\n            btn.style.padding = mods.ui.padding\n            btn.style.margin = 1\n            btn.addEventListener('click',function(){\n               win.close()\n               })\n            win.document.body.appendChild(btn)\n         win.document.body.appendChild(document.createElement('br'))\n         var canvas = document.createElement('canvas')\n            canvas.width = mod.img.width\n            canvas.height = mod.img.height\n            win.document.body.appendChild(canvas)\n         var ctx = canvas.getContext(\"2d\")\n            ctx.drawImage(mod.img,0,0)\n         })\n      div.appendChild(btn)\n   }\n//\n// local functions\n//\n// find limits then map \n//\nfunction find_limits_map() {\n   var blob = new Blob(['('+limits_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      mod.triangles = evt.data.triangles\n      mod.xmin = evt.data.xmin\n      mod.xmax = evt.data.xmax\n      mod.ymin = evt.data.ymin\n      mod.ymax = evt.data.ymax\n      mod.zmin = evt.data.zmin\n      mod.zmax = evt.data.zmax\n      mod.dx = mod.xmax-mod.xmin\n      mod.dy = mod.ymax-mod.ymin\n      mod.dz = mod.zmax-mod.zmin\n      mod.meshsize.nodeValue = \n         mod.dx.toFixed(3)+' x '+\n         mod.dy.toFixed(3)+' x '+\n         mod.dz.toFixed(3)+' (units)'\n      var mm = parseFloat(mod.mmunits.value)\n      mod.mmsize.nodeValue = \n         (mod.dx*mm).toFixed(3)+' x '+\n         (mod.dy*mm).toFixed(3)+' x '+\n         (mod.dz*mm).toFixed(3)+' (mm)'\n      var inches = parseFloat(mod.inunits.value)\n      mod.insize.nodeValue = \n         (mod.dx*inches).toFixed(3)+' x '+\n         (mod.dy*inches).toFixed(3)+' x '+\n         (mod.dz*inches).toFixed(3)+' (in)'\n      mods.fit(mod.div)\n      map_mesh()\n      })\n   var border = parseFloat(mod.border.value)\n   webworker.postMessage({\n      mesh:mod.mesh,\n      border:border})\n   }\nfunction limits_worker() {\n   self.addEventListener('message',function(evt) {\n      var view = evt.data.mesh\n      var border = evt.data.border\n      //\n      // get vars\n      //\n      var endian = true\n      var triangles = view.getUint32(80,endian)\n      var size = 80+4+triangles*(4*12+2)\n      //\n      // find limits\n      //\n      var offset = 80+4\n      var x0,x1,x2,y0,y1,y2,z0,z1,z2\n      var xmin = Number.MAX_VALUE\n      var xmax = -Number.MAX_VALUE\n      var ymin = Number.MAX_VALUE\n      var ymax = -Number.MAX_VALUE\n      var zmin = Number.MAX_VALUE\n      var zmax = -Number.MAX_VALUE\n      for (var t = 0; t < triangles; ++t) {\n         offset += 3*4\n         x0 = view.getFloat32(offset,endian)\n         offset += 4\n         y0 = view.getFloat32(offset,endian)\n         offset += 4\n         z0 = view.getFloat32(offset,endian)\n         offset += 4\n         x1 = view.getFloat32(offset,endian)\n         offset += 4\n         y1 = view.getFloat32(offset,endian)\n         offset += 4\n         z1 = view.getFloat32(offset,endian)\n         offset += 4\n         x2 = view.getFloat32(offset,endian)\n         offset += 4\n         y2 = view.getFloat32(offset,endian)\n         offset += 4\n         z2 = view.getFloat32(offset,endian)\n         offset += 4\n         offset += 2\n         if (x0 > xmax) xmax = x0\n         if (x0 < xmin) xmin = x0\n         if (y0 > ymax) ymax = y0\n         if (y0 < ymin) ymin = y0\n         if (z0 > zmax) zmax = z0\n         if (z0 < zmin) zmin = z0\n         if (x1 > xmax) xmax = x1\n         if (x1 < xmin) xmin = x1\n         if (y1 > ymax) ymax = y1\n         if (y1 < ymin) ymin = y1\n         if (z1 > zmax) zmax = z1\n         if (z1 < zmin) zmin = z1\n         if (x2 > xmax) xmax = x2\n         if (x2 < xmin) xmin = x2\n         if (y2 > ymax) ymax = y2\n         if (y2 < ymin) ymin = y2\n         if (z2 > zmax) zmax = z2\n         if (z2 < zmin) zmin = z2\n         }\n      xmin -= border\n      xmax += border\n      ymin -= border\n      ymax += border\n      //\n      // return\n      //\n      self.postMessage({triangles:triangles,\n         xmin:xmin,xmax:xmax,ymin:ymin,ymax:ymax,\n         zmin:zmin,zmax:zmax})\n      self.close()\n      })\n   }\n//\n// map mesh\n//   \nfunction map_mesh() {\n   var blob = new Blob(['('+map_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.imgbuffer)\n      var map = new Float32Array(evt.data.mapbuffer)\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.mapcanvas.height*.5*(1-h/w)\n         var wd = mod.mapcanvas.width\n         var hd = mod.mapcanvas.width*h/w\n         }\n      else {\n         var x0 = mod.mapcanvas.width*.5*(1-w/h)\n         var y0 = 0\n         var wd = mod.mapcanvas.height*w/h\n         var hd = mod.mapcanvas.height\n         }\n      var ctx = mod.mapcanvas.getContext(\"2d\")\n      ctx.clearRect(0,0,mod.mapcanvas.width,mod.mapcanvas.height)\n      ctx.drawImage(mod.img,x0,y0,wd,hd)\n      outputs.map.event(map)\n      })\n   var ctx = mod.mapcanvas.getContext(\"2d\")\n   ctx.clearRect(0,0,mod.mapcanvas.width,mod.mapcanvas.height)\n   mod.img.width = parseInt(mod.width.value)\n   mod.img.height = Math.round(mod.img.width*mod.dy/mod.dx)\n   var ctx = mod.img.getContext(\"2d\")\n   var img = ctx.getImageData(0,0,mod.img.width,mod.img.height)\n   var map = new Float32Array(mod.img.width*mod.img.height)\n   webworker.postMessage({\n      height:mod.img.height,width:mod.img.width,\n      imgbuffer:img.data.buffer,\n      mapbuffer:map.buffer,\n      mesh:mod.mesh,\n      xmin:mod.xmin,xmax:mod.xmax,\n      ymin:mod.ymin,ymax:mod.ymax,\n      zmin:mod.zmin,zmax:mod.zmax},\n      [img.data.buffer,map.buffer])\n   }\nfunction map_worker() {\n   self.addEventListener('message',function(evt) {\n      var h = evt.data.height\n      var w = evt.data.width\n      var view = evt.data.mesh\n      var xmin = evt.data.xmin\n      var xmax = evt.data.xmax\n      var ymin = evt.data.ymin\n      var ymax = evt.data.ymax\n      var zmin = evt.data.zmin\n      var zmax = evt.data.zmax\n      var buf = new Uint8ClampedArray(evt.data.imgbuffer)\n      var map = new Float32Array(evt.data.mapbuffer)\n      //\n      // get vars from buffer\n      //\n      var endian = true\n      var triangles = view.getUint32(80,endian)\n      var size = 80+4+triangles*(4*12+2)\n      //\n      // initialize map and image\n      //\n      for (var row = 0; row < h; ++row) {\n         for (var col = 0; col < w; ++col) {\n            map[(h-1-row)*w+col] = zmin\n            buf[(h-1-row)*w*4+col*4+0] = 0\n            buf[(h-1-row)*w*4+col*4+1] = 0\n            buf[(h-1-row)*w*4+col*4+2] = 0\n            buf[(h-1-row)*w*4+col*4+3] = 255\n            }\n         }\n      //\n      // loop over triangles\n      //\n      var segs = []\n      offset = 80+4\n      for (var t = 0; t < triangles; ++t) {\n         offset += 3*4\n         x0 = view.getFloat32(offset,endian)\n         offset += 4\n         y0 = view.getFloat32(offset,endian)\n         offset += 4\n         z0 = view.getFloat32(offset,endian)\n         offset += 4\n         x1 = view.getFloat32(offset,endian)\n         offset += 4\n         y1 = view.getFloat32(offset,endian)\n         offset += 4\n         z1 = view.getFloat32(offset,endian)\n         offset += 4\n         x2 = view.getFloat32(offset,endian)\n         offset += 4\n         y2 = view.getFloat32(offset,endian)\n         offset += 4\n         z2 = view.getFloat32(offset,endian)\n         offset += 4\n         offset += 2\n         //\n         // check normal if needs to be drawn\n         //\n         if (((x1-x0)*(y1-y2)-(x1-x2)*(y1-y0)) >= 0)\n            continue\n         //\n         // quantize image coordinates\n         //\n         x0 = Math.floor((w-1)*(x0-xmin)/(xmax-xmin))\n         x1 = Math.floor((w-1)*(x1-xmin)/(xmax-xmin))\n         x2 = Math.floor((w-1)*(x2-xmin)/(xmax-xmin))\n         y0 = Math.floor((h-1)*(y0-ymin)/(ymax-ymin))\n         y1 = Math.floor((h-1)*(y1-ymin)/(ymax-ymin))\n         y2 = Math.floor((h-1)*(y2-ymin)/(ymax-ymin))\n         //\n         // sort projection order\n         //\n         if (y1 > y2) {\n            var temp = x1;\n            x1 = x2;\n            x2 = temp\n            var temp = y1;\n            y1 = y2;\n            y2 = temp\n            var temp = z1;\n            z1 = z2;\n            z2 = temp\n            }\n         if (y0 > y1) {\n            var temp = x0;\n            x0 = x1;\n            x1 = temp\n            var temp = y0;\n            y0 = y1;\n            y1 = temp\n            var temp = z0;\n            z0 = z1;\n            z1 = temp\n            }\n         if (y1 > y2) {\n            var temp = x1;\n            x1 = x2;\n            x2 = temp\n            var temp = y1;\n            y1 = y2;\n            y2 = temp\n            var temp = z1;\n            z1 = z2;\n            z2 = temp\n            }\n         //\n         // check orientation after sort\n         //\n         if (x1 < (x0+((x2-x0)*(y1-y0))/(y2-y0)))\n            var dir = 1;\n         else\n            var dir = -1;\n         //\n         // set z values\n         //\n         if (y2 != y1) {\n            for (var y = y1; y <= y2; ++y) {\n               x12 = Math.floor(0.5+x1+(y-y1)*(x2-x1)/(y2-y1))\n               z12 = z1+(y-y1)*(z2-z1)/(y2-y1)\n               x02 = Math.floor(0.5+x0+(y-y0)*(x2-x0)/(y2-y0))\n               z02 = z0+(y-y0)*(z2-z0)/(y2-y0)\n               if (x12 != x02)\n                  var slope = (z02-z12)/(x02-x12)\n               else\n                  var slope = 0\n               var x = x12 - dir\n               while (x != x02) {\n                  x += dir\n                  var z = z12+slope*(x-x12)\n                  if (z > map[(h-1-y)*w+x]) {\n                     map[(h-1-y)*w+x] = z\n                     var iz = Math.floor(255*(z-zmin)/(zmax-zmin))\n                     buf[(h-1-y)*w*4+x*4+0] = iz\n                     buf[(h-1-y)*w*4+x*4+1] = iz\n                     buf[(h-1-y)*w*4+x*4+2] = iz\n                     }\n                  }\n               }\n            }\n         if (y1 != y0) {\n            for (var y = y0; y <= y1; ++y) {\n               x01 = Math.floor(0.5+x0+(y-y0)*(x1-x0)/(y1-y0))\n               z01 = z0+(y-y0)*(z1-z0)/(y1-y0)\n               x02 = Math.floor(0.5+x0+(y-y0)*(x2-x0)/(y2-y0))\n               z02 = z0+(y-y0)*(z2-z0)/(y2-y0)\n               if (x01 != x02)\n                  var slope = (z02-z01)/(x02-x01)\n               else\n                  var slope = 0\n               var x = x01 - dir\n               while (x != x02) {\n                  x += dir\n                  var z = z01+slope*(x-x01)\n                  if (z > map[(h-1-y)*w+x]) {\n                     map[(h-1-y)*w+x] = z\n                     var iz = Math.floor(255*(z-zmin)/(zmax-zmin))\n                     buf[(h-1-y)*w*4+x*4+0] = iz\n                     buf[(h-1-y)*w*4+x*4+1] = iz\n                     buf[(h-1-y)*w*4+x*4+2] = iz\n                     }\n                  }\n               }\n            }\n         }\n      //\n      // output the map\n      //\n      self.postMessage({imgbuffer:buf.buffer,mapbuffer:map.buffer},[buf.buffer,map.buffer])\n      self.close()\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":"228.40801751128484","left":"523.2621169675592","inputs":{},"outputs":{}},"0.536212242526729":{"definition":"//\n// mill raster 3D\n//\n// Neil Gershenfeld 1/18/20\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 = 'mill raster 3D (incomplete)'\n//\n// initialization\n//\nvar init = function() {\n   mod.dia_in.value = '0.0156'\n   mod.dia_mm.value = '0.39624'\n   mod.stepover.value = '0.5'\n   mod.error.value = '1'\n   }\n//\n// inputs\n//\nvar inputs = {\n   map:{type:'',label:'height map',\n      event:function(evt){\n         mod.map = evt.detail.map\n         mod.width = evt.detail.width\n         mod.height = evt.detail.height\n         mod.xmin = evt.detail.xmin\n         mod.xmax = evt.detail.xmax\n         mod.ymin = evt.detail.ymin\n         mod.ymax = evt.detail.ymax\n         mod.zmin = evt.detail.zmin\n         mod.zmax = evt.detail.zmax\n         var ctx = mod.img.getContext(\"2d\")\n         ctx.canvas.width = mod.width\n         ctx.canvas.height = mod.height\n         }}}\n//\n// outputs\n//\nvar outputs = {\n   toolpath:{type:'',\n      event:function(){\n         cmd = {}\n         cmd.path = mod.path\n         cmd.name = mod.name\n         cmd.dpi = mod.dpi\n         cmd.width = mod.width\n         cmd.height = mod.height\n         cmd.depth = mod.depth\n         mods.output(mod,'toolpath',cmd)\n         }}}\n//\n// interface\n//\nvar interface = function(div){\n   mod.div = div\n   //\n   // tool diameter\n   //\n   div.appendChild(document.createTextNode('tool diameter'))\n   div.appendChild(document.createElement('br'))\n   div.appendChild(document.createTextNode('mm: '))\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 6\n      input.addEventListener('input',function(){\n         mod.dia_in.value = parseFloat(mod.dia_mm.value)/25.4\n         })\n      div.appendChild(input)\n      mod.dia_mm = input\n   div.appendChild(document.createTextNode(' in: '))\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 6\n      input.addEventListener('input',function(){\n         mod.dia_mm.value = parseFloat(mod.dia_in.value)*25.4\n         })\n      div.appendChild(input)\n      mod.dia_in = input\n   div.appendChild(document.createElement('br'))\n   //\n   // stepover\n   //\n   div.appendChild(document.createTextNode('stepover (0-1): '))\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 6\n      div.appendChild(input)\n      mod.stepover = input\n   div.appendChild(document.createElement('br'))\n   //\n   // tool shape\n   //\n   div.appendChild(document.createTextNode('tool shape: '))\n   var input = document.createElement('input')\n      input.type = 'radio'\n      input.name = mod.div.id+'shape'\n      input.id = mod.div.id+'flatend'\n      input.checked = true\n      div.appendChild(input)\n      mod.flatend= input\n   div.appendChild(document.createTextNode('flat end'))\n   div.appendChild(document.createElement('br'))\n   //\n   // direction \n   //\n   div.appendChild(document.createTextNode('direction: '))\n   var input = document.createElement('input')\n      input.type = 'radio'\n      input.name = mod.div.id+'direction'\n      input.id = mod.div.id+'dirx'\n      input.checked = true\n      div.appendChild(input)\n      mod.dirx = input\n   div.appendChild(document.createTextNode('xz'))\n   div.appendChild(document.createElement('br'))\n   //\n   // fit error \n   //\n   div.appendChild(document.createTextNode('vector fit (pixels): '))\n   //div.appendChild(document.createElement('br'))\n   var input = document.createElement('input')\n      input.type = 'text'\n      input.size = 6\n      input.addEventListener('change',function(){\n         vectorize()\n         })\n      div.appendChild(input)\n      mod.error = input\n   div.appendChild(document.createElement('br'))\n   //\n   // calculate\n   //\n   var btn = document.createElement('button')\n      btn.style.padding = mods.ui.padding\n      btn.style.margin = 1\n      var span = document.createElement('span')\n         var text = document.createTextNode('calculate')\n            mod.label = text\n            span.appendChild(text)\n         mod.labelspan = span\n         btn.appendChild(span)\n      btn.addEventListener('click',function(){\n         mod.label.nodeValue = 'calculating'\n         mod.labelspan.style.fontWeight = 'bold'\n         calculate_path()\n         })\n      div.appendChild(btn)\n   div.appendChild(document.createTextNode(' '))\n   //\n   // view\n   //\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         var win = window.open('')\n         var btn = document.createElement('button')\n            btn.appendChild(document.createTextNode('close'))\n            btn.style.padding = mods.ui.padding\n            btn.style.margin = 1\n            btn.addEventListener('click',function(){\n               win.close()\n               })\n            win.document.body.appendChild(btn)\n         win.document.body.appendChild(document.createElement('br'))\n         var svg = document.getElementById(mod.div.id+'svg')\n         var clone = svg.cloneNode(true)\n         clone.setAttribute('width',mod.img.width)\n         clone.setAttribute('height',mod.img.height)\n         win.document.body.appendChild(clone)\n         })\n      div.appendChild(btn)\n   div.appendChild(document.createElement('br'))\n   //\n   // on-screen SVG\n   //\n   var svgNS = \"http://www.w3.org/2000/svg\"\n   var svg = document.createElementNS(svgNS,\"svg\")\n   svg.setAttribute('id',mod.div.id+'svg')\n   svg.setAttributeNS(\"http://www.w3.org/2000/xmlns/\",\n      \"xmlns:xlink\",\"http://www.w3.org/1999/xlink\")\n   svg.setAttribute('width',mods.ui.canvas)\n   svg.setAttribute('height',mods.ui.canvas)\n   svg.style.backgroundColor = 'rgb(255,255,255)'\n   var g = document.createElementNS(svgNS,'g')\n   g.setAttribute('id',mod.div.id+'g')\n   svg.appendChild(g)\n   div.appendChild(svg)\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//\n// local functions\n//\n// calculate_path\n//\nfunction calculate_path() {\n   var h = mod.height\n   var w = mod.width\n   var xmin = mod.xmin\n   var xmax = mod.xmax\n   var ymin = mod.ymin\n   var ymax = mod.ymax\n   var zmin = mod.zmin\n   var zmax = mod.zmax\n   //\n   // clear SVG\n   //\n   var svg = document.getElementById(mod.div.id+'svg')\n   svg.setAttribute('viewBox',\"0 0 \"+(w-1)+\" \"+(h-1))\n   var g = document.getElementById(mod.div.id+'g')\n   svg.removeChild(g)\n   var g = document.createElementNS('http://www.w3.org/2000/svg','g')\n   g.setAttribute('id',mod.div.id+'g')\n   svg.appendChild(g)\n   //\n   // line loop\n   //\n   var ix = 0\n   var iy = h-1\n   var dx = 1\n   var dy = 0\n   var x = xmin+(xmax-xmin)*ix/(w-1)\n   var y = ymin+(ymax-ymin)*iy/(h-1)\n   var z = mod.map[(h-1-iy)*w+ix]\n   var dz = h*(zmax-z)/(ymax-ymin)\n   while (1) {\n      var ixp = ix\n      var iyp = iy\n      var dzp = dz\n      ix += dx\n      iy += dy\n      if (iy <= 0)\n         break;\n      var x = xmin+(xmax-xmin)*ix/(w-1)\n      var y = ymin+(ymax-ymin)*iy/(h-1)\n      var z = mod.map[iy*w+ix]\n      var dz = 0.1*h*(zmax-z)/(zmax-zmin)\n      var line = document.createElementNS('http://www.w3.org/2000/svg','line')\n         line.setAttribute('stroke','black')\n         line.setAttribute('stroke-width',1)\n         line.setAttribute('stroke-linecap','round')\n         line.setAttribute('x1',ixp)\n         line.setAttribute('y1',iyp+dzp)\n         line.setAttribute('x2',ix)\n         line.setAttribute('y2',iy+dz)\n         g.appendChild(line)\n      if (ix == (mod.width-1)) {\n         if (dx == 1) {\n            dx = 0\n            dy = -10\n            }\n         else {\n            dx = -1\n            dy = 0\n            }\n         }\n      else if (ix == 0) {\n         if (dx == -1) {\n            dx = 0\n            dy = -10\n            }\n         else {\n            dx = 1\n            dy = 0\n            }\n         }\n      }\n   mod.label.nodeValue = 'calculate'\n   mod.labelspan.style.fontWeight = 'normal'\n   }\n\n//\n// clear_path\n//\nfunction clear_path() {\n   }\n//\n// accumulate_path\n//    todo: replace inefficient insertion sort\n//    todo: move sort out of main thread\n//\nfunction accumulate_path(path) {\n   var forward = mod.forward.checked\n   var conventional = mod.conventional.checked\n   var sort = mod.sort.checked\n   for (var segnew = 0; segnew < path.length; ++segnew) {\n      if (conventional)\n         path[segnew].reverse()\n      if (mod.path.length == 0)\n         mod.path.splice(0,0,path[segnew])\n      else if (sort) {\n         var xnew = path[segnew][0][0]\n         var ynew = path[segnew][0][1]\n         var dmin = Number.MAX_VALUE\n         var segmin = -1\n         for (var segold = 0; segold < mod.path.length; ++segold) {\n            var xold = mod.path[segold][0][0]\n            var yold = mod.path[segold][0][1]\n            var dx = xnew-xold\n            var dy = ynew-yold\n            var d = Math.sqrt(dx*dx+dy*dy)\n            if (d < dmin) {\n               dmin = d\n               segmin = segold\n               }\n            }\n         if (forward)\n            mod.path.splice(segmin+1,0,path[segnew])\n         else\n            mod.path.splice(segmin,0,path[segnew])\n         }\n      else {\n         if (forward)\n            mod.path.splice(mod.path.length,0,path[segnew])\n         else\n            mod.path.splice(0,0,path[segnew])\n         }\n      }\n   }\n//\n// merge_path\n//\nfunction merge_path() {\n   var dmerge = mod.dpi*parseFloat(mod.merge.value)*parseFloat(mod.dia_in.value)\n   var seg = 0\n   while (seg < (mod.path.length-1)) {\n      var xold = mod.path[seg][mod.path[seg].length-1][0]\n      var yold = mod.path[seg][mod.path[seg].length-1][1]\n      var xnew = mod.path[seg+1][0][0]\n      var ynew = mod.path[seg+1][0][1]\n      var dx = xnew-xold\n      var dy = ynew-yold\n      var d = Math.sqrt(dx*dx+dy*dy)\n      if (d < dmerge)\n         mod.path.splice(seg,2,mod.path[seg].concat(mod.path[seg+1]))\n      else\n         seg += 1\n      }\n   }\n//\n// add_depth\n//\nfunction add_depth() {\n   var cut = parseFloat(mod.cut_in.value)\n   var max = parseFloat(mod.max_in.value)\n   var newpath = []\n   for (var seg = 0; seg < mod.path.length; ++seg) {\n      var depth = cut\n      if (mod.path[seg][0][0] == mod.path[seg][mod.path[seg].length-1][0]) {\n         var newseg = []\n         while (depth <= max) {\n            var idepth = -Math.round(mod.dpi*depth)\n            for (var pt = 0; pt < mod.path[seg].length; ++pt) {\n               var point = mod.path[seg][pt].concat(idepth)\n               newseg.splice(newseg.length,0,point)\n               }\n            if (depth == max)\n               break\n            depth += cut\n            if (depth > max)\n               depth = max\n            }\n         newpath.splice(newpath.length,0,newseg)\n         }\n      else {\n         var newseg = []\n         while (depth <= max) {\n            var idepth = -Math.round(mod.dpi*depth)\n            for (var pt = 0; pt < mod.path[seg].length; ++pt) {\n               var point = mod.path[seg][pt].concat(idepth)\n               newseg.splice(newseg.length,0,point)\n               }\n            newpath.splice(newpath.length,0,newseg)\n            newseg = []\n            if (depth == max)\n               break\n            depth += cut\n            if (depth > max)\n               depth = max\n            }\n         }\n      }\n   mod.path = newpath\n   mod.depth = Math.round(parseFloat(mod.max_in.value)*mod.dpi)\n   }\n//\n// draw_path\n//\nfunction draw_path(path) {\n   var g = document.getElementById(mod.div.id+'g')\n   var h = mod.img.height\n   var w = mod.img.width\n   var xend = null\n   var yend = null\n   //\n   // loop over segments\n   //\n   for (var segment = 0; segment < path.length; ++segment) {\n      if (path[segment].length > 1) {\n         //\n         // loop over points\n         //\n         for (var point = 1; point < path[segment].length; ++point) {\n            var line = document.createElementNS('http://www.w3.org/2000/svg','line')\n            line.setAttribute('stroke','black')\n            line.setAttribute('stroke-width',1)\n            line.setAttribute('stroke-linecap','round')\n            var x1 = path[segment][point-1][0]\n            var y1 = h-path[segment][point-1][1]-1\n            var x2 = path[segment][point][0]\n            var y2 = h-path[segment][point][1]-1\n            xend = x2\n            yend = y2\n            line.setAttribute('x1',x1)\n            line.setAttribute('y1',y1)\n            line.setAttribute('x2',x2)\n            line.setAttribute('y2',y2)\n            var dx = x2-x1\n            var dy = y2-y1\n            var d = Math.sqrt(dx*dx+dy*dy)\n            if (d > 0) {\n               nx = 6*dx/d\n               ny = 6*dy/d\n               var tx = 3*dy/d\n               var ty = -3*dx/d\n               g.appendChild(line)\n               triangle = document.createElementNS('http://www.w3.org/2000/svg','polygon')\n               triangle.setAttribute('points',x2+','+y2+' '+(x2-nx+tx)+','+(y2-ny+ty)\n                  +' '+(x2-nx-tx)+','+(y2-ny-ty))\n               triangle.setAttribute('fill','black')\n               g.appendChild(triangle)\n               }\n            }\n         }\n      }\n   }\n//\n// draw_connections\n//\nfunction draw_connections() {\n   var g = document.getElementById(mod.div.id+'g')\n   var h = mod.img.height\n   var w = mod.img.width\n   //\n   // loop over segments\n   //\n   for (var segment = 1; segment < mod.path.length; ++segment) {\n      //\n      // draw connection from previous segment\n      //\n      var line = document.createElementNS('http://www.w3.org/2000/svg','line')\n      line.setAttribute('stroke','red')\n      line.setAttribute('stroke-width',1)\n      line.setAttribute('stroke-linecap','round')\n      var x1 = mod.path[segment-1][mod.path[segment-1].length-1][0]\n      var y1 = h-mod.path[segment-1][mod.path[segment-1].length-1][1]-1\n      var x2 = mod.path[segment][0][0]\n      var y2 = h-mod.path[segment][0][1]-1\n      line.setAttribute('x1',x1)\n      line.setAttribute('y1',y1)\n      line.setAttribute('x2',x2)\n      line.setAttribute('y2',y2)\n      var dx = x2-x1\n      var dy = y2-y1\n      var d = Math.sqrt(dx*dx+dy*dy)\n      if (d > 0) {\n         nx = 6*dx/d\n         ny = 6*dy/d\n         var tx = 3*dy/d\n         var ty = -3*dx/d\n         g.appendChild(line)\n         triangle = document.createElementNS('http://www.w3.org/2000/svg','polygon')\n         triangle.setAttribute('points',x2+','+y2+' '+(x2-nx+tx)+','+(y2-ny+ty)\n            +' '+(x2-nx-tx)+','+(y2-ny-ty))\n         triangle.setAttribute('fill','red')\n         g.appendChild(triangle)\n         }\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\n","top":"112.66259275401453","left":"950.4095131219412","inputs":{},"outputs":{}}},"links":["{\"source\":\"{\\\"id\\\":\\\"0.9903638182304415\\\",\\\"type\\\":\\\"outputs\\\",\\\"name\\\":\\\"mesh\\\"}\",\"dest\":\"{\\\"id\\\":\\\"0.7269098240824425\\\",\\\"type\\\":\\\"inputs\\\",\\\"name\\\":\\\"mesh\\\"}\"}","{\"source\":\"{\\\"id\\\":\\\"0.7269098240824425\\\",\\\"type\\\":\\\"outputs\\\",\\\"name\\\":\\\"map\\\"}\",\"dest\":\"{\\\"id\\\":\\\"0.536212242526729\\\",\\\"type\\\":\\\"inputs\\\",\\\"name\\\":\\\"map\\\"}\"}"]}
\ No newline at end of file