<html>
<title>Split STL mesh</title>
<body>
<p>This is a simple piece of javascript (licensed under the MIT license) by
<a href="http://www.thingiverse.com/arpruss/">Alexander Pruss</a> that splits an STL mesh file that contains multiple parts into
those multiple parts.</p>
<p>This may well fail if the mesh is defective and has points that are meant to be coincident 
but are merely close together.</p>
<p>All the processing is done in your browser--your STL is not uploaded to any server.</p>
<input type="file" id="file" name="file" />
<ul id="console">
</ul>
<p id="progress">
</p>


<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) {
        function fixZero(a) {
            return a == 0x80000000 ? 0 : a;
        }
        x = fixZero(view.getUint32(position, true));
        y = fixZero(view.getUint32(position+4, true));
        z = fixZero(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;
}

var splitMeshIndex;
var meshes;

function splitMesh(triangles) {
    meshes = [];
    splitMeshIndex = 0;
    
    function process() {
        var i = splitMeshIndex;
    
        document.getElementById('progress').innerHTML = "Splitting "+(i/triangles.length*100).toFixed(1)+'% done ('+(meshes.length)+' parts found)';

        for (; 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);
            
            if (i && i % 5000 == 0) {
                setTimeout(process, 0);
                splitMeshIndex = i+1;
                return;
            }
        }

        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);

        if (meshes.length == 1) {
            message("No splitting done: Only one mesh in file.");
        }
        else if (meshes.length == 0) {
            message("No mesh found in file.");
        }
        else {
            message(String(meshes.length)+" meshes extracted");
            allSplitMeshes = [];
            
            for (var i=0; i<meshes.length; i++) {
                allSplitMeshes.push(meshes[i].triangles);
                message("<a href='#' onclick='downloadMesh("+i+");'>Download mesh part "+i+"</a> "+describeBounds(meshes[i].bounds));
            }
        }

        document.getElementById('progress').innerHTML = '';
        document.getElementById('file').disabled = false;
    }
    
    process();
}

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();
    link.remove();
    document.body.removeChild(link);
}

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");    
    splitMesh(triangles);
}

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

    var f = evt.target.files[0];
    
    message( "reading "+ 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>