diff --git a/files.html b/files.html index d95bf984c3be18329209822e0461e10d7c08d58e..7739d47abd97386d5b5a45c22ed41c73b8b4fc7e 100644 --- a/files.html +++ b/files.html @@ -137,6 +137,7 @@ <a href='./modules/math/parallel'>parallel</a><br> <a href='./modules/math/scalar'>scalar</a><br> <i> mesh</i><br> + <a href='./modules/mesh/height%20map'>height map</a><br> <a href='./modules/mesh/rotate'>rotate</a><br> <a href='./modules/mesh/slice%20raster'>slice raster</a><br> <i> module</i><br> diff --git a/modules/index.js b/modules/index.js index 111e510642811b6e9ef7dc0992e5073863b88f0f..fad1081cbe1e2ba45a4e984a4c3e77d59ff2923f 100644 --- a/modules/index.js +++ b/modules/index.js @@ -106,6 +106,7 @@ module_menu(' glsl_benchmark','modules/math/glsl_benchmark') module_menu(' parallel','modules/math/parallel') module_menu(' scalar','modules/math/scalar') module_label('mesh') +module_menu(' height map','modules/mesh/height%20map') module_menu(' rotate','modules/mesh/rotate') module_menu(' slice raster','modules/mesh/slice%20raster') module_label('module') diff --git a/modules/mesh/height map b/modules/mesh/height map new file mode 100644 index 0000000000000000000000000000000000000000..977e0195d7664f4b5da87f9183e9b6de27e82efe --- /dev/null +++ b/modules/mesh/height map @@ -0,0 +1,500 @@ +// +// mesh height map +// +// Neil Gershenfeld +// 1/16/20 +// +// This work may be reproduced, modified, distributed, performed, and +// displayed for any purpose, but must acknowledge the mods +// project. Copyright is retained and must be preserved. The work is +// provided as is; no warranty is provided, and users accept all +// liability. +// +// closure +// +(function(){ +// +// module globals +// +var mod = {} +// +// name +// +var name = 'mesh height map' +// +// initialization +// +var init = function() { + mod.mmunits.value = 25.4 + mod.inunits.value = 1 + mod.width.value = 1000 + mod.border.value = 0 + mod.delta = 1e-6 + } +// +// inputs +// +var inputs = { + mesh:{type:'STL', + event:function(evt){ + mod.mesh = new DataView(evt.detail) + find_limits_map()}}, + settings:{type:'', + event:function(evt){ + for (var p in evt.detail) + ; + find_limits_map()}}} +// +// outputs +// +var outputs = { + map:{type:'F32', + event:function(){ + }}, + image:{type:'RGBA', + event:function(){ + var ctx = mod.img.getContext("2d") + var img = ctx.getImageData(0,0,mod.img.width,mod.img.height) + mods.output(mod,'image',img) + }}, + imageInfo:{type:'', + event:function(){ + var obj = {} + obj.name = "mesh height map" + obj.width = mod.img.width + obj.height = mod.img.height + obj.dpi = mod.img.width/(mod.dx*parseFloat(mod.inunits.value)) + mods.output(mod,'imageInfo',obj) + }}} +// +// interface +// +var interface = function(div){ + mod.div = div + // + // on-screen height map canvas + // + div.appendChild(document.createTextNode(' ')) + var canvas = document.createElement('canvas') + canvas.width = mods.ui.canvas + canvas.height = mods.ui.canvas + canvas.style.backgroundColor = 'rgb(255,255,255)' + div.appendChild(canvas) + mod.mapcanvas = canvas + div.appendChild(document.createElement('br')) + // + // off-screen image canvas + // + var canvas = document.createElement('canvas') + mod.img = canvas + // + // mesh units + // + div.appendChild(document.createTextNode('mesh units: (enter)')) + div.appendChild(document.createElement('br')) + div.appendChild(document.createTextNode('mm: ')) + var input = document.createElement('input') + input.type = 'text' + input.size = 6 + input.addEventListener('change',function(){ + mod.inunits.value = parseFloat(mod.mmunits.value)/25.4 + find_limits_map() + }) + div.appendChild(input) + mod.mmunits = input + div.appendChild(document.createTextNode(' in: ')) + var input = document.createElement('input') + input.type = 'text' + input.size = 6 + input.addEventListener('change',function(){ + mod.mmunits.value = parseFloat(mod.inunits.value)*25.4 + find_limits_map() + }) + div.appendChild(input) + mod.inunits = input + // + // mesh size + // + div.appendChild(document.createElement('br')) + div.appendChild(document.createTextNode('mesh size:')) + div.appendChild(document.createElement('br')) + var text = document.createTextNode('XxYxZ (units)') + div.appendChild(text) + mod.meshsize = text + div.appendChild(document.createElement('br')) + var text = document.createTextNode('XxYxZ (mm)') + div.appendChild(text) + mod.mmsize = text + div.appendChild(document.createElement('br')) + var text = document.createTextNode('XxYxZ (in)') + div.appendChild(text) + mod.insize = text + // + // height map border + // + div.appendChild(document.createElement('br')) + div.appendChild(document.createTextNode('border: ')) + var input = document.createElement('input') + input.type = 'text' + input.size = 6 + input.addEventListener('change',function(){ + find_limits_map() + }) + div.appendChild(input) + mod.border = input + div.appendChild(document.createTextNode(' (units)')) + // + // height map width + // + div.appendChild(document.createElement('br')) + div.appendChild(document.createTextNode('width: ')) + var input = document.createElement('input') + input.type = 'text' + input.size = 6 + input.addEventListener('change',function(){ + find_limits_map() + }) + div.appendChild(input) + mod.width = input + div.appendChild(document.createTextNode(' (pixels)')) + // + // view height map + // + div.appendChild(document.createElement('br')) + var btn = document.createElement('button') + btn.style.padding = mods.ui.padding + btn.style.margin = 1 + btn.appendChild(document.createTextNode('view height map')) + btn.addEventListener('click',function(){ + var win = window.open('') + var btn = document.createElement('button') + btn.appendChild(document.createTextNode('close')) + btn.style.padding = mods.ui.padding + btn.style.margin = 1 + btn.addEventListener('click',function(){ + win.close() + }) + win.document.body.appendChild(btn) + win.document.body.appendChild(document.createElement('br')) + var canvas = document.createElement('canvas') + canvas.width = mod.img.width + canvas.height = mod.img.height + win.document.body.appendChild(canvas) + var ctx = canvas.getContext("2d") + ctx.drawImage(mod.img,0,0) + }) + div.appendChild(btn) + } +// +// local functions +// +// find limits then map +// +function find_limits_map() { + var blob = new Blob(['('+limits_worker.toString()+'())']) + var url = window.URL.createObjectURL(blob) + var webworker = new Worker(url) + webworker.addEventListener('message',function(evt) { + window.URL.revokeObjectURL(url) + mod.triangles = evt.data.triangles + mod.xmin = evt.data.xmin + mod.xmax = evt.data.xmax + mod.ymin = evt.data.ymin + mod.ymax = evt.data.ymax + mod.zmin = evt.data.zmin + mod.zmax = evt.data.zmax + mod.dx = mod.xmax-mod.xmin + mod.dy = mod.ymax-mod.ymin + mod.dz = mod.zmax-mod.zmin + mod.meshsize.nodeValue = + mod.dx.toFixed(3)+' x '+ + mod.dy.toFixed(3)+' x '+ + mod.dz.toFixed(3)+' (units)' + var mm = parseFloat(mod.mmunits.value) + mod.mmsize.nodeValue = + (mod.dx*mm).toFixed(3)+' x '+ + (mod.dy*mm).toFixed(3)+' x '+ + (mod.dz*mm).toFixed(3)+' (mm)' + var inches = parseFloat(mod.inunits.value) + mod.insize.nodeValue = + (mod.dx*inches).toFixed(3)+' x '+ + (mod.dy*inches).toFixed(3)+' x '+ + (mod.dz*inches).toFixed(3)+' (in)' + mods.fit(mod.div) + map_mesh() + }) + var border = parseFloat(mod.border.value) + webworker.postMessage({ + mesh:mod.mesh, + border:border,delta:mod.delta}) + } +function limits_worker() { + self.addEventListener('message',function(evt) { + var view = evt.data.mesh + var border = evt.data.border + var delta = evt.data.delta // perturb to remove degeneracies + // + // get vars + // + var endian = true + var triangles = view.getUint32(80,endian) + var size = 80+4+triangles*(4*12+2) + // + // find limits + // + var offset = 80+4 + var x0,x1,x2,y0,y1,y2,z0,z1,z2 + var xmin = Number.MAX_VALUE + var xmax = -Number.MAX_VALUE + var ymin = Number.MAX_VALUE + var ymax = -Number.MAX_VALUE + var zmin = Number.MAX_VALUE + var zmax = -Number.MAX_VALUE + for (var t = 0; t < triangles; ++t) { + offset += 3*4 + x0 = view.getFloat32(offset,endian)+delta + offset += 4 + y0 = view.getFloat32(offset,endian)+delta + offset += 4 + z0 = view.getFloat32(offset,endian)+delta + offset += 4 + x1 = view.getFloat32(offset,endian)+delta + offset += 4 + y1 = view.getFloat32(offset,endian)+delta + offset += 4 + z1 = view.getFloat32(offset,endian)+delta + offset += 4 + x2 = view.getFloat32(offset,endian)+delta + offset += 4 + y2 = view.getFloat32(offset,endian)+delta + offset += 4 + z2 = view.getFloat32(offset,endian)+delta + offset += 4 + offset += 2 + if (x0 > xmax) xmax = x0 + if (x0 < xmin) xmin = x0 + if (y0 > ymax) ymax = y0 + if (y0 < ymin) ymin = y0 + if (z0 > zmax) zmax = z0 + if (z0 < zmin) zmin = z0 + if (x1 > xmax) xmax = x1 + if (x1 < xmin) xmin = x1 + if (y1 > ymax) ymax = y1 + if (y1 < ymin) ymin = y1 + if (z1 > zmax) zmax = z1 + if (z1 < zmin) zmin = z1 + if (x2 > xmax) xmax = x2 + if (x2 < xmin) xmin = x2 + if (y2 > ymax) ymax = y2 + if (y2 < ymin) ymin = y2 + if (z2 > zmax) zmax = z2 + if (z2 < zmin) zmin = z2 + } + xmin -= border + xmax += border + ymin -= border + ymax += border + // + // return + // + self.postMessage({triangles:triangles, + xmin:xmin,xmax:xmax,ymin:ymin,ymax:ymax, + zmin:zmin,zmax:zmax}) + self.close() + }) + } +// +// map mesh +// +function map_mesh() { + var blob = new Blob(['('+map_worker.toString()+'())']) + var url = window.URL.createObjectURL(blob) + var webworker = new Worker(url) + webworker.addEventListener('message',function(evt) { + window.URL.revokeObjectURL(url) + var h = mod.img.height + var w = mod.img.width + var buf = new Uint8ClampedArray(evt.data.buffer) + var imgdata = new ImageData(buf,w,h) + var ctx = mod.img.getContext("2d") + ctx.putImageData(imgdata,0,0) + if (w > h) { + var x0 = 0 + var y0 = mod.mapcanvas.height*.5*(1-h/w) + var wd = mod.mapcanvas.width + var hd = mod.mapcanvas.width*h/w + } + else { + var x0 = mod.mapcanvas.width*.5*(1-w/h) + var y0 = 0 + var wd = mod.mapcanvas.height*w/h + var hd = mod.mapcanvas.height + } + var ctx = mod.mapcanvas.getContext("2d") + ctx.clearRect(0,0,mod.mapcanvas.width,mod.mapcanvas.height) + ctx.drawImage(mod.img,x0,y0,wd,hd) + outputs.image.event() + outputs.imageInfo.event() + }) + var ctx = mod.mapcanvas.getContext("2d") + ctx.clearRect(0,0,mod.mapcanvas.width,mod.mapcanvas.height) + mod.img.width = parseInt(mod.width.value) + mod.img.height = Math.round(mod.img.width*mod.dy/mod.dx) + var ctx = mod.img.getContext("2d") + var img = ctx.getImageData(0,0,mod.img.width,mod.img.height) + webworker.postMessage({ + height:mod.img.height,width:mod.img.width, + imgbuffer:img.data.buffer,mesh:mod.mesh, + xmin:mod.xmin,xmax:mod.xmax, + ymin:mod.ymin,ymax:mod.ymax, + zmin:mod.zmin,zmax:mod.zmax, + delta:mod.delta}, + [img.data.buffer]) + } +function map_worker() { + self.addEventListener('message',function(evt) { + var h = evt.data.height + var w = evt.data.width + var view = evt.data.mesh + var delta = evt.data.delta // perturb to remove degeneracies + var xmin = evt.data.xmin + var xmax = evt.data.xmax + var ymin = evt.data.ymin + var ymax = evt.data.ymax + var zmin = evt.data.zmin + var zmax = evt.data.zmax + var buf = new Uint8ClampedArray(evt.data.imgbuffer) + // + // get vars from buffer + // + var endian = true + var triangles = view.getUint32(80,endian) + var size = 80+4+triangles*(4*12+2) + // + // initialize map image + // + for (var row = 0; row < h; ++row) { + for (var col = 0; col < w; ++col) { + buf[(h-1-row)*w*4+col*4+0] = row + buf[(h-1-row)*w*4+col*4+1] = col + buf[(h-1-row)*w*4+col*4+2] = 0 + buf[(h-1-row)*w*4+col*4+3] = 255 + } + } + /* + // + // find triangles crossing the map + // + var segs = [] + offset = 80+4 + for (var t = 0; t < triangles; ++t) { + offset += 3*4 + x0 = view.getFloat32(offset,endian)+delta + offset += 4 + y0 = view.getFloat32(offset,endian)+delta + offset += 4 + z0 = view.getFloat32(offset,endian)+delta + offset += 4 + x1 = view.getFloat32(offset,endian)+delta + offset += 4 + y1 = view.getFloat32(offset,endian)+delta + offset += 4 + z1 = view.getFloat32(offset,endian)+delta + offset += 4 + x2 = view.getFloat32(offset,endian)+delta + offset += 4 + y2 = view.getFloat32(offset,endian)+delta + offset += 4 + z2 = view.getFloat32(offset,endian)+delta + offset += 4 + // + // assemble vertices + // + offset += 2 + var v = [[x0,y0,z0],[x1,y1,z1],[x2,y2,z2]] + // + // sort z + // + v.sort(function(a,b) { + if (a[2] < b[2]) + return -1 + else if (a[2] > b[2]) + return 1 + else + return 0 + }) + // + // check for crossings + // + if ((v[0][2] < (zmax-depth)) && (v[2][2] > (zmax-depth))) { + // + // crossing found, check for side and save + // + if (v[1][2] < (zmax-depth)) { + var x0 = v[2][0]+(v[0][0]-v[2][0]) + *(v[2][2]-(zmax-depth))/(v[2][2]-v[0][2]) + var y0 = v[2][1]+(v[0][1]-v[2][1]) + *(v[2][2]-(zmax-depth))/(v[2][2]-v[0][2]) + var x1 = v[2][0]+(v[1][0]-v[2][0]) + *(v[2][2]-(zmax-depth))/(v[2][2]-v[1][2]) + var y1 = v[2][1]+(v[1][1]-v[2][1]) + *(v[2][2]-(zmax-depth))/(v[2][2]-v[1][2]) + } + else if (v[1][2] >= (zmax-depth)) { + var x0 = v[2][0]+(v[0][0]-v[2][0]) + *(v[2][2]-(zmax-depth))/(v[2][2]-v[0][2]) + var y0 = v[2][1]+(v[0][1]-v[2][1]) + *(v[2][2]-(zmax-depth))/(v[2][2]-v[0][2]) + var x1 = v[1][0]+(v[0][0]-v[1][0]) + *(v[1][2]-(zmax-depth))/(v[1][2]-v[0][2]) + var y1 = v[1][1]+(v[0][1]-v[1][1]) + *(v[1][2]-(zmax-depth))/(v[1][2]-v[0][2]) + } + if (y0 < y1) + segs.push({x0:x0,y0:y0,x1:x1,y1:y1}) + else + segs.push({x0:x1,y0:y1,x1:x0,y1:y0}) + } + } + // + // fill interior + // + for (var row = 0; row < h; ++row) { + var y = ymin+(ymax-ymin)*row/(h-1) + rowsegs = segs.filter(p => ((p.y0 <= y) && (p.y1 >= y))) + var xs = rowsegs.map(p => + (p.x0+(p.x1-p.x0)*(y-p.y0)/(p.y1-p.y0))) + xs.sort((a,b) => (a-b)) + for (var col = 0; col < w; ++col) { + var x = xmin+(xmax-xmin)*col/(w-1) + var index = xs.findIndex((p) => (p >= x)) + if (index == -1) + var i = 0 + else + var i = 255*(index%2) + buf[(h-1-row)*w*4+col*4+0] = i + buf[(h-1-row)*w*4+col*4+1] = i + buf[(h-1-row)*w*4+col*4+2] = i + buf[(h-1-row)*w*4+col*4+3] = 255 + } + } + */ + // + // output the map + // + self.postMessage({buffer:buf.buffer},[buf.buffer]) + self.close() + }) + } +// +// return values +// +return ({ + mod:mod, + name:name, + init:init, + inputs:inputs, + outputs:outputs, + interface:interface + }) +}())