<html>
<body>
<input type="file" id="file" name="file" />
<ul id="console">
</ul>


<script>

var TRIANGLE_SIZE = (3+3*3)*4+2;
var ASCII_MODE = true;
var allSplitMeshes = [];
var baseFilaname = "";

function getString(view, position, length) {
    var out = "";
    
    for (var i=0; i<length; i++) {
        var b = view.getUint8(position + i);
        out += String.fromCharCode(b);
    }
    
    return out;
}

// For hashing during splitting, ASCII mode is used, where vectors are stored
// as strings. From a binary STL, the vectors are stored as hex strings, and
// from an ASCII STL, they are stored as directly extracted ASCII.

function getVector(view, position) {
    if (ASCII_MODE) {
        x = view.getUint32(position, true);
        y = view.getUint32(position+4, true);
        z = view.getUint32(position+8, true);
        return x.toString(16)+":"+y.toString(16)+":"+z.toString(16);
    }

    x = view.getFloat32(position, true);
    y = view.getFloat32(position+4, true);
    z = view.getFloat32(position+8, true);
    return [x,y,z]; // x.toString()+","+y.toString()+","+z.toString(); //[x,y,z];
}

function parseVector(vector) {
    if (typeof vector === "string") {
        var data = vector.split(":");
        var buf = new ArrayBuffer(4);
        var view = new DataView(buf);
        function parse(s) {
            view.setUint32(0, parseInt(s,16));
            return view.getFloat32(0);
        }
        return [parse(data[0]),parse(data[1]),parse(data[2])];
    }
    else {
        return vector;
    }
}

function setVector(view, position, vector) {
    if (typeof vector === "string") {
        var data = vector.split(":");
        view.setUint32(position, parseInt(data[0],16), true);
        view.setUint32(position+4, parseInt(data[1],16), true);
        view.setUint32(position+8, parseInt(data[2],16), true);
    }
    else {
        view.setFloat32(position, vector[0], true);
        view.setFloat32(position+4, vector[1], true);
        view.setFloat32(position+8, vector[2], true);
    }
}

function getVectorFromText(line, position) {
    var data = line.substr(position).split(/[\s,]+/);
    
    if (data.length < 3)
        throw 'Invalid vector';
        
    if (ASCII_MODE) {
        var buf = new ArrayBuffer(4);
        var view = new DataView(buf);

        function toHex32(number) {
            view.setFloat32(0, parseFloat(number));
            return view.getUint32(0).toString(16);
        }

        return toHex32(data[0])+":"+toHex32(data[1])+":"+toHex32(data[2]);
    }

    return [parseFloat(data[0]), parseFloat(data[1]), parseFloat(data[2])];
}

function message(text) {
    document.getElementById('console').innerHTML += '<li>'+text+'</li>';
}

function getASCIISTL(text) {
    var triangles = [];
    
    var triangle = [];
    var normal = [0,0,0];
    
    var lines = text.split(/[\r\n]+/);
    
    message(lines.length+" lines of data");
    
    for (var i = 0 ; i < lines.length ; i++ ) {
        l = lines[i].trim().toLowerCase();
        if (l == 'endfacet') {
            if (triangle.length != 3) 
                throw 'invalid triangle';
            triangles.push([triangle[0],triangle[1],triangle[2],normal]);
            triangle = [];
            normal = [0,0,0];
        }
        else if (l.startsWith('facet')) {
            if (l.length > 6) {
                if (l.substr(6).startsWith('normal'))
                    normal = getVectorFromText(l, 13);
            }
        }
        else if (l.startsWith('vertex')) {
            if (l.length > 7) 
                triangle.push(getVectorFromText(l, 7));
        }
    }
    
    message(String(triangles.length) + " triangles");
    
    return triangles;
}

function getBinarySTL(view) {
    var triangles = [];

    var numTriangles = view.getUint32(80, true);
    
    message(String(numTriangles) + " triangles");
    
    for (var i = 0 ; i < numTriangles ; i++) {
        position = 84 + TRIANGLE_SIZE*i;
    
        normal = getVector(view, position);
        v1 = getVector(view, position+12);
        v2 = getVector(view, position+12*2);
        v3 = getVector(view, position+12*3);
        triangles.push( [v1,v2,v3,normal] );
    }
    
    return triangles;
}

function splitMesh(triangles) {
    meshes = [];
    
    for (var i = 0; i<triangles.length; i++) {
        var t = triangles[i];
        var matches = [];
        for (var j = 0 ; j < meshes.length; j++) {
            for (var k = 0 ; k < 3 ; k++) {
                if (t[k] in meshes[j].points) {
                    matches.push(j);
                    break;
                }
            }
        }
        var m;
        if (matches.length == 0) {
            m = {points:{}, triangles:[]};
            meshes.push(m);
        }
        else {
            m = meshes[matches[0]];
            for (var j = matches.length - 1 ; j >= 1 ; j--) {
                mm = meshes[matches[j]];
                for (var key in mm.points) {
                    if (mm.points.hasOwnProperty(key))
                        m.points[key] = true;
                }
                for (var k = 0 ; k < mm.triangles.length; k++) {
                    m.triangles.push(mm.triangles[k]);
                }
                meshes.splice(matches[j], 1);
            }
        }
        for (var k = 0 ; k < 3 ; k++) {
            m.points[t[k]] = true;
        }
        m.triangles.push(t);
    }
    
    for (var i = 0 ; i < meshes.length ; i++) {
        var bounds = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, 
                Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY ];

        for (var key in meshes[i].points) {
            if (meshes[i].points.hasOwnProperty(key)) {
                var v = parseVector(key);
                for (var k=0; k<3; k++) {
                    bounds[k] = Math.min(bounds[k], v[k]);
                    bounds[3+k] = Math.max(bounds[3+k], v[k]);
                }
            }
        }
    
        meshes[i].bounds = bounds;
        delete meshes[i].points;
    }
    
    function compareByBounds(m,m) {
        for (var i=0; i<6; i++) {
            if (m.bounds[i] < m.bounds[i])
                return -1;
            else if (m.bounds[i] < m.bounds[i])
                return -1;
        }
        return 0;
    }
    
    meshes.sort(compareByBounds);
    
    return meshes;
}

function downloadBlob(name,blob) {
    var link = document.createElement('a');
    link.download = name;
    link.href = window.URL.createObjectURL(blob);
    link.onclick = function(e) {
        setTimeout(function() {
            window.URL.revokeObjectURL(link.href);
        }, 1600);
    };
    link.click();
    link.remove();
}

function makeMeshByteArray(triangles) {
    data = new ArrayBuffer(84 + triangles.length * TRIANGLE_SIZE);
    view = new DataView(data);
    view.setUint32(80, triangles.length, true);
    for (var i=0; i<triangles.length; i++) {
        var offset = 84 + i*TRIANGLE_SIZE;
        setVector(view, offset, triangles[i][3]); // normal
        setVector(view, offset+12, triangles[i][0]); // v1
        setVector(view, offset+12*2, triangles[i][1]); // v2
        setVector(view, offset+12*3, triangles[i][2]); // v3
    }
    return view.buffer;
}

function downloadMesh(i) {
    downloadBlob(baseFilename+i+".stl", new Blob([makeMeshByteArray(allSplitMeshes[i])], {type: "application/octet-stream"}));
}

function describeBounds(bounds) {
    return "("+bounds[0].toFixed(2)+", "+bounds[1].toFixed(2)+", "+bounds[2].toFixed(2)+") - ("+
            bounds[3].toFixed(2)+", "+bounds[4].toFixed(2)+", "+bounds[5].toFixed(2)+")";
}

function processSTL(data) {
    length = data.byteLength;
    view = new DataView(data);
    
    var header = getString(view, 0, 5);
    
    var binary = true;
    var text;
    
    if (header == "solid") {
        // probably ASCII
        text = getString(view, 0, length);
        if (text.includes("endfacet")) {
            binary = false;
        }
    }
    
    message(binary ? "binary STL" : "ASCII STL");
    triangles = binary ? getBinarySTL(view) : getASCIISTL(text);
    message("data successfully read");    
    splitMeshes = splitMesh(triangles);
    if (splitMeshes.length == 1) {
        message("No splitting done: Only one mesh in file.");
    }
    else if (splitMeshes.length == 0) {
        message("No mesh found in file.");
    }
    else {
        message(String(splitMeshes.length)+" meshes extracted");
        allSplitMeshes = [];
        
        for (var i=0; i<splitMeshes.length; i++) {
            allSplitMeshes.push(splitMeshes[i].triangles);
            message("<a href='#' onclick='downloadMesh("+i+");'>Download mesh part "+i+"</a> "+describeBounds(splitMeshes[i].bounds));
        }
    }
}

function handleFileSelect(evt) {
    var e = document.getElementById('file');
    e.disabled = true;
    document.getElementById('console').innerHTML = '';

    var f = evt.target.files[0];
    
    message( String(f.size) + ' bytes');
    
    var n = f.name.split(/[/\\]+/);
    baseFilename = f.name.replace(/.*[/\\]/, "").replace(/\.[sS][tT][lL]$/, "");

    var reader = new FileReader();
    reader.onload = function(event) {
        try {
            processSTL(event.target.result);
        }
        catch(err) {
            message( "Error: "+err);
        }
        var e = document.getElementById('file');
        e.disabled = false;
    }
    reader.readAsArrayBuffer(f);
}

document.getElementById('file').addEventListener('change', handleFileSelect, false);
</script>
</body>
</html>