Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
511 lines (441 sloc) 16.7 KB
<!DOCTYPE html>
<html>
<head><title>Snowflake to STL</title></head>
</head>
<body onload="load()">
<table >
<tr title="Name from Martin Krzywinski's website">
<th>Snowflake name</th>
<td><input id="name" type="text" size="10" value="morptel"/></td>
<td>(find it <a href="http://mkweb.bcgsc.ca/snowflakes">here</a>)</td>
</tr>
<tr title="Diameter of snowflake STL to be generated">
<th>Diameter</th>
<td><input id="diameter" type="number" size="10" value="190" min="0.1" step="5" max="10000"/></td>
<td>mm</td>
</tr>
<tr title="Thickness (height) of thinnest parts of snowflake">
<th>Minimum thickness</th>
<td><input id="thinnest" type="number" size="10" step="0.1" value="1.25" min="0.1" max="10000"/></td>
<td>mm</td>
</tr>
<tr title="Thickness (height) of thickest parts of snowflake">
<th>Maximum thickness</th>
<td><input id="thickest" type="number" size="10" step="0.1" value="2.25" min="0.1" max="10000"/></td>
<td>mm</td>
</tr>
<tr title="Add these many cells around a given cell to make thin tendrils more printable">
<th>Tendril widening distance</th>
<td><input id="widen" type="number" size="10" step="0.25" min="0" max="30" value="2"/></td>
<td>cells</td>
</tr>
<tr title="Number of vertical levels to generate">
<th>Number of levels</th>
<td><input id="levels" type="number" size="10" step="1" min="1" max="20" value="8"/></td>
</tr>
<tr title="Reduce resolution to reduce size of STL file">
<th>Resolution reduction factor</th>
<td><input id="reduce" type="number" size="10" step="0.1" min="1" max="10" value="1"/></td>
</tr>
</table>
<button id="Go" onClick="generate()">Generate STL</button><br/><br/></p>
<p>Source code is <a href="https://github.com/arpruss/pyflakes/blob/master/js/snowflake.html">here</a>.</p>
<div id="output"></div><br/>
<div id="progress"></div>
</body>
<script>
function corsCircumvent(url) {
//return "http://crossorigin.me/"+encodeURIComponent(url)
//return "http://cors-proxy.htmldriven.com/?url="+encodeURIComponent(url)
return "https://api.allorigins.ml/get?method=raw&url="+encodeURIComponent(url)+"&callback=?"
}
var TRIANGLE_SIZE = (3+3*3)*4+2
var diameter
var minThickness
var maxThickness
var levels
var name
var reduce
var widen
var CORNERS = [[1,1], [-1,2], [-2,1], [-1,-1], [1,-2], [2,-1]];
function distance(x0,y0,x1,y1) {
x1 -= x0
y1 -= y0
return Math.sqrt(Math.pow((x1+0.5*y1)*3,2)+Math.pow((y1*1.5)*1.5,2))
}
function displayX(x,y,size) {
x -= size*1.5
y -= size*1.5
return (x+0.5*y)*Math.sqrt(3) // ((x-2*size) + (y-2*size) / 2.) * Math.sqrt(3)
}
function displayY(x,y,size) {
y -= size*1.5
return y*1.5 // (y-2*size) * 1.5
}
function getScale(raster, size) {
var maxRadiusSq = 0;
for (var x = 0 ; x < size ; x++)
for (var y = 0; y < size ; y++)
if (raster[x][y]>0)
maxRadiusSq = Math.max(maxRadiusSq,Math.pow(displayX(x*3,y*3,size),2)+Math.pow(displayY(x*3,y*3,size),2))
console.log(diameter,2*(0.5+Math.sqrt(maxRadiusSq)))
return diameter/(2*(0.5+Math.sqrt(maxRadiusSq)))
}
function addLevelToMesh(mesh, segmentSet, raster, size, scale, prevHeight, height, prevThickness, thickness, startX, endX) {
var maxCoord = 100000
if (startX == 0)
segmentSet.clear()
function xyToDisplay(xy) {
return [scale*displayX(xy[0],xy[1],size),scale*displayY(xy[0],xy[1],size)]
}
function raiseTo(a,h) {
return [a[0],a[1],h]
}
function addTriangle(a,b,c) {
var tri = new Float32Array(9)
for (var i=0 ; i < 3 ; i++) {
tri[i] = a[i]
}
for (var i=0 ; i < 3 ; i++) {
tri[3+i] = b[i]
}
for (var i=0 ; i < 3 ; i++) {
tri[6+i] = c[i]
}
mesh.push(tri)
}
function addTriangleAt(h, a, b, c) {
addTriangle(raiseTo(a,h),raiseTo(b,h),raiseTo(c,h))
}
function addSideMesh(a, b) {
addTriangle(raiseTo(a,prevThickness), raiseTo(b,prevThickness), raiseTo(b,thickness))
addTriangle(raiseTo(a,prevThickness), raiseTo(b,thickness), raiseTo(a,thickness))
}
for (var x = startX ; x < endX; x++)
for (var y = 0 ; y < size ; y++) {
if (prevHeight < raster[x][y]) {
var center = xyToDisplay([3*x,3*y])
for (var i=0; i<6; i++) {
var c0 = CORNERS[i]
var c1 = CORNERS[(i+1)%6]
var p0 = [3*x+c0[0],3*y+c0[1]]
var p1 = [3*x+c1[0],3*y+c1[1]]
var rev = p1[0]+","+p1[1]+","+p0[0]+","+p0[1]
if (segmentSet.has(rev)) {
segmentSet.delete(rev)
}
else {
segmentSet.add(p0[0]+","+p0[1]+","+p1[0]+","+p1[1])
}
if (raster[x][y] <= height) {
addTriangleAt(0, center, xyToDisplay(p1), xyToDisplay(p0))
addTriangleAt(thickness, center, xyToDisplay(p0), xyToDisplay(p1))
}
}
}
}
if (endX < size)
return
console.log("Meshing edges")
segmentSet.forEach(function(seg) {
var points = seg.split(",").map(Number)
addSideMesh(xyToDisplay([points[0],points[1]]), xyToDisplay([points[2],points[3]]))
})
}
function processRaster(raster, size) {
window.document.getElementById("progress").innerHTML = "Meshing..."
var scale = getScale(raster, size)
var minHeight = Number.POSITIVE_INFINITY
var maxHeight = Number.NEGATIVE_INFINITY
for (var x = 0 ; x < size ; x++) {
for (var y = 0; y<size ; y++) {
if (raster[x][y] > 0) {
minHeight = Math.min(minHeight, raster[x][y])
maxHeight = Math.max(maxHeight, raster[x][y])
}
}
}
var segmentSet = new Set()
var mesh = []
function addLevel(level,startX,endX) {
var thickness
if (levels == 1) {
thickness = maxThickness
}
else {
thickness = minThickness+level*(maxThickness-minThickness)/(levels-1)
}
var height = minHeight+(level+1)*(maxHeight-minHeight)/levels
var prevHeight
var prevThickness
if (level == 0) {
prevHeight = 0
prevThickness = 0
}
else {
prevThickness = minThickness+(level-1)*(maxThickness-minThickness)/(levels-1)
prevHeight = minHeight+level*(maxHeight-minHeight)/levels
}
console.log("Mesh level "+(level+1)+"/"+levels+" "+prevHeight+" "+height)
if (level == levels-1)
height = Number.POSITIVE_INFINITY;
addLevelToMesh(mesh, segmentSet, raster, size, scale, prevHeight, height, prevThickness, thickness, startX, endX)
prevHeight = height
prevThickness = thickness
}
var substeps = 10
function processStep(level,substep) {
if (level<levels) {
console.log("level",level,"substep",substep,"of",substeps)
var startX = Math.floor(size*substep/substeps)
var endX = Math.floor(size*(substep+1)/substeps)
addLevel(level,Math.floor(size*substep/substeps),Math.floor(size*(substep+1)/substeps))
substep++
window.document.getElementById("progress").innerHTML = "Level "+(level+1)+" of "+levels+" ("+Math.floor(100*substep/substeps)+"%)"
if (substep >= substeps) {
substep = 0
level++
}
setTimeout(processStep, 10, level, substep)
}
else {
console.log("meshing done",mesh.length)
window.document.getElementById("progress").innerHTML = "Preparing download..."
setTimeout(function() {
downloadBlob((name[0]=='_' ? name.substring(1) : name)+".stl", new Blob([makeMeshByteArray(mesh)], {type: "application/octet-stream"}));
},0)
}
}
window.document.getElementById("progress").innerHTML = "Level 1 of "+levels
setTimeout(processStep, 0, 0, 0)
}
function processRunfile(response) {
var lines = response.split(/\r?\n/)
window.document.getElementById("progress").innerHTML = "Processing runfile..."
var size = 0
var raster
message = ""
for (var i = 0; i < lines.length; i++) {
var line = lines[i].split(" ")
if (line[0] == "run" && line[1]=="j") {
message += "steps: "+line[2]+"<br/>"
}
else if (line[0] == "param") {
var params = { "a": "alpha", "b": "beta", "g": "gamma", "k": "kappa", "m": "mu", "r": "rho", "s": "sigma", "n": "resolution" }
if (params[line[1]]) {
message += params[line[1]]+": "+line[2]+"<br/>"
if (line[1] == "n") {
size = parseInt(line[2])
raster = []
for (var x = 0 ; x < size ; x++) {
raster[x] = []
for (var y = 0 ; y < size ; y++)
raster[x][y] = 0
}
}
}
}
else if (line[0]=='y') {
var y = Math.floor(parseInt(line[1])/reduce)
for (var x = 0 ; x < size ; x++) {
raster[x][y] = (x==size/2 && y==size/2) ? 255 : parseInt(line[2+x])
}
}
}
console.log("runfile processed")
if (! size) {
window.document.getElementById("output").innerHTML = "Badly formed data."
return
}
else {
window.document.getElementById("output").innerHTML = message
}
if (widen > 0) {
window.document.getElementById("progress").innerHTML = "widening data."
}
setTimeout( function() { widenRaster(raster, size) }, 0 )
}
function widenRaster(raster, size) {
if (widen > 0) {
var r = 3*widen + 0.1
console.log("widen radius",r)
var out = []
for (var x = 0 ; x < size ; x++) {
out[x] = []
for (var y = 0; y < size ; y++) {
out[x][y] = raster[x][y]
if (out[x][y] <= 0) {
var newValue = Number.POSITIVE_INFINITY
for (var x1 = Math.max(0,x-widen) ; x1 <= x+widen && x1 < size; x1++)
for (var y1 = Math.max(0,y-widen) ; y1 <= y+widen && y1 < size ; y1++) {
var v = raster[x1][y1]
if (0 < v && v < newValue && distance(x,y,x1,y1) <= r) {
newValue = v
}
}
if (newValue < Number.POSITIVE_INFINITY)
out[x][y] = newValue
}
}
}
raster = out
}
if (reduce > 1) {
window.document.getElementById("progress").innerHTML = "Reducing resolution."
}
setTimeout( function() { reduceRaster(raster, size) }, 0 )
}
function reduceRaster(raster, size) {
if (reduce > 1) {
var newSize = Math.floor( (size-1) / reduce) + 1
var out = []
for (var x = 0 ; x < newSize ; x++) {
out[x] = []
for (var y = 0; y < newSize ; y++)
out[x][y] = 0
}
for (var x = 0 ; x < size ; x++) {
var x1 = Math.floor(x / reduce)
for (var y = 0; y < size ; y++) {
var y1 = Math.floor(y / reduce)
out[x1][y1] = Math.max(out[x1][y1], raster[x][y])
}
}
raster = out
size = newSize
}
window.document.getElementById("progress").innerHTML = "Generating mesh."
setTimeout( function() { processRaster(raster, size) }, 0 )
}
function ready() {
document.getElementById('Go').disabled = false
}
function getRunfile(internalName) {
var xhr = new XMLHttpRequest()
xhr.onload = function() {
processRunfile(xhr.response)
}
xhr.onerror = function() {
window.document.getElementById("progress").innerHTML = "Error loading snowflake data."
ready()
}
url = "http://mkweb.bcgsc.ca/snowflakes/flakes/snowflake-"+internalName+".txt"
xhr.open("GET", corsCircumvent(url))
xhr.responseType = "text"
xhr.send()
window.document.getElementById("progress").innerHTML = "Loading..."
}
function process(document) {
console.log("Scraping")
var found = false;
if (document) {
links = document.getElementsByTagName('a')
for (var i=0;i<links.length;i++) {
matches = links[i].href.match("/flakes/snowflake-(.*)\\.txt")
if (matches) {
found = true
getRunfile(matches[1])
break
}
}
}
if (!found) {
window.document.getElementById("progress").innerHTML = "Snowflake not found or error in connecting."
ready()
}
}
function load() {
var args = window.location.search.substr(1).split('&')
for (var i=0;i<args.length;i++) {
var parts = args[i].split('=')
if (parts.length == 2 && 0 <= ['name', 'diameter', 'thinnest', 'thickest', 'widen', 'levels', 'reduce'].indexOf(parts[0]) ) {
var value = decodeURIComponent(parts[1])
if (parts[0] == 'name')
value = '_' + value
window.document.getElementById(parts[0]).value = value
}
}
}
function generate() {
window.document.getElementById("output").innerHTML = ""
diameter = parseFloat(window.document.getElementById("diameter").value)
minThickness = parseFloat(window.document.getElementById("thinnest").value)
maxThickness = parseFloat(window.document.getElementById("thickest").value)
levels = parseInt(window.document.getElementById("levels").value)
reduce = parseInt(window.document.getElementById("reduce").value)
widen = parseFloat(window.document.getElementById("widen").value)
name = window.document.getElementById("name").value.toLowerCase()
if (levels < 1 || minThickness >= maxThickness)
levels = 1
if (name[0] == '_') {
getRunfile(name.substring(1))
}
else {
var xhr = new XMLHttpRequest()
xhr.onload = function() {
process(xhr.responseXML)
}
xhr.onerror = function() {
window.document.getElementById("progress").innerHTML = "Snowflake not found or error in connecting."
ready()
}
url = "http://mkweb.bcgsc.ca/snowflakes/flake.mhtml?flake="+name
xhr.open("GET", corsCircumvent(url))
xhr.responseType = "document"
xhr.send()
document.getElementById("progress").innerHTML = "Searching..."
}
document.getElementById("output").innerHTML = ""
document.getElementById('Go').disabled = true
// processRaster([[]], 800)
}
/*
function setVector(view, position, vector) {
view.setFloat32(position, vector[0], true);
view.setFloat32(position+4, vector[1], true);
view.setFloat32(position+8, vector[2], true);
}
*/
function makeMeshByteArray(triangles) {
totalTriangles = triangles.length
var data = new ArrayBuffer(84 + totalTriangles * TRIANGLE_SIZE);
var view = new DataView(data);
view.setUint32(80, totalTriangles, true);
var offset = 84;
for (var i=0; i<totalTriangles; i++) {
for (var j = 0; j<3 ; j++) {
view.setFloat32(offset, 0, true)
offset += 4
}
for (var j = 0; j<9 ; j++) {
view.setFloat32(offset, triangles[i][j], true)
offset += 4
}
offset += 2
}
console.log (offset,totalTriangles,84 + totalTriangles * TRIANGLE_SIZE)
return view.buffer;
}
function downloadBlob(name,blob) {
var link = document.createElement('a');
document.body.appendChild(link);
link.download = name;
link.href = window.URL.createObjectURL(blob);
link.onclick = function(e) {
setTimeout(function() {
window.URL.revokeObjectURL(link.href);
}, 1600);
};
link.click();
try {
link.remove();
}
catch(err) {}
try {
document.body.removeChild(link);
}
catch(err) {}
ready()
}
</script>
</html>
You can’t perform that action at this time.