491 lines
18 KiB
JavaScript
491 lines
18 KiB
JavaScript
/**
|
|
* @author Junho Jin[junho.jin@kaist.ac.kr] | https://github.com/JinJunho
|
|
* @version 1.0.0
|
|
*
|
|
* [Project] Madeleine.js, Pure JavaScript STL Parser & Renderer.
|
|
* [Module] MadeleineLoader.js
|
|
*
|
|
* [Description] MadeleineLoader takes raw arrayBuffer data of a stl file and
|
|
* parses the data properly by checking if its format is binary of ASCII. For
|
|
* better performance, MadeleineLoader runs in the background, not affecting
|
|
* any performance degradation of browser rendering or script execution.
|
|
*/
|
|
|
|
var workerFacadeMessage;
|
|
|
|
MadeleineLoader = function(event) {
|
|
|
|
var arrbuf = event.data.arrbuf;
|
|
var rawText = event.data.rawtext;
|
|
var reader = new DataReader(arrbuf);
|
|
|
|
var maxX = 0, maxY = 0, maxZ = 0;
|
|
var minX = 0, minY = 0, minZ = 0;
|
|
var avgX = 0, avgY = 0, avgZ = 0;
|
|
var centerX, centerY, centerZ;
|
|
var normals, vertices;
|
|
var facets = 0;
|
|
|
|
var checkSTLType, parseBinarySTL, parseASCIISTL;
|
|
var hasColors = false, colors, alpha;
|
|
var normalX, normalY, normalZ;
|
|
var vertexX, vertexY, vertexZ;
|
|
|
|
// STL file type checker
|
|
checkSTLType = function() {
|
|
var realSize, dataSize, isBinary;
|
|
|
|
// NOTE:
|
|
// Because STL files don't provide any information whether they are ASCII or binary,
|
|
// We have to distinguish them by looking up the file structure.
|
|
// - REFERENCE: http://en.wikipedia.org/wiki/STL_(file_format)
|
|
// STL binary file starts with 80-character header.
|
|
// (If a file starts with "solid" (literally), it's mostly accepted as an ASCII STL file)
|
|
// After header, 4 bytes unsigned ingeter follows. (number of triangular facets in the file)
|
|
// Now the data (triangular facets) starts, where each facet consists of 50 bytes.
|
|
// First 12 bytes : 32-bit normal vector
|
|
// Following 36 bytes : 32-bit vertex for 3 coordinates (X|Y|Z)
|
|
// Last 2 bytes : short unsigned integer for "attribute byte count"
|
|
// So if the file is Binary STL file, then the size must be
|
|
// 80 + 4 + 50 * number of triangular facets
|
|
|
|
reader.skip(80);
|
|
|
|
// reader.readUInt32() == number of facets
|
|
dataSize = 80 + 4 + 50 * reader.readUInt32();
|
|
realSize = reader.getLength();
|
|
isBinary = dataSize == realSize;
|
|
|
|
// Send data type
|
|
workerFacadeMessage({
|
|
type: "info",
|
|
prop: "type",
|
|
data: isBinary ? "binary" : "ascii"
|
|
});
|
|
|
|
// Send data size
|
|
workerFacadeMessage({
|
|
type: "info",
|
|
prop: "size",
|
|
data: (function(bytes) {
|
|
var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
if (bytes == 0) return '0 Byte';
|
|
var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
|
|
return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i];
|
|
})(realSize)
|
|
});
|
|
|
|
return isBinary;
|
|
};
|
|
|
|
// Parse binary stl file
|
|
parseBinarySTL = function() {
|
|
var color, colorR, colorG, colorB;
|
|
var defaultR, defaultG, defaultB;
|
|
var index, i, j;
|
|
|
|
// Point head of reader.
|
|
reader.reset();
|
|
|
|
// Get color information from the first 80 bytes if exists.
|
|
for (i = 0; i < 80 - 10; i++) {
|
|
if ((reader.getUInt32(i, false) == 0x434F4C4F /*COLO*/) && (reader.getUInt8(i + 4) == 0x52 /*'R'*/) && (reader.getUInt8(index + 5) == 0x3D /*'='*/)) {
|
|
hasColors = true;
|
|
colors = new Float32Array(facets * 3 * 3);
|
|
defaultR = reader.getUInt8(i + 6) / 255;
|
|
defaultG = reader.getUInt8(i + 7) / 255;
|
|
defaultB = reader.getUInt8(i + 8) / 255;
|
|
alpha = reader.getUInt8(i + 9) / 255;
|
|
}
|
|
}
|
|
reader.skip(80);
|
|
|
|
// Get the number of triangular facets, and initialize
|
|
// normals and vertices with the size of facets.
|
|
index = 0;
|
|
facets = reader.readUInt32();
|
|
normals = new Float32Array(facets * 3 * 3);
|
|
vertices = new Float32Array(facets * 3 * 3);
|
|
|
|
// iterate triangular facets
|
|
for (i = 0; i < facets; i++) {
|
|
// Send current progress
|
|
if (i % 100 == 0) {
|
|
workerFacadeMessage({
|
|
type: "progress",
|
|
data: Math.round((i/facets) * 100)
|
|
});
|
|
}
|
|
// Get color information from attribute bytes
|
|
if (hasColors) {
|
|
color = reader.getUInt16(reader.getCurrIndex() + 48);
|
|
if ((color & 0x8000) === 0) { // facet has its own unique color
|
|
colorR = (color & 0x1F) / 31;
|
|
colorG = ((color >> 5) & 0x1F) / 31;
|
|
colorB = ((color >> 10) & 0x1F) / 31;
|
|
} else {
|
|
colorR = defaultR;
|
|
colorG = defaultG;
|
|
colorB = defaultB;
|
|
}
|
|
}
|
|
// Read normal vector
|
|
normalX = reader.readFloat32();
|
|
normalY = reader.readFloat32();
|
|
normalZ = reader.readFloat32();
|
|
// Get vertices for each coordinate
|
|
for (j = 0; j < 3; j++) {
|
|
// Add vertex vector
|
|
vertexX = reader.readFloat32();
|
|
vertexY = reader.readFloat32();
|
|
vertexZ = reader.readFloat32();
|
|
vertices[index] = vertexX;
|
|
vertices[index + 1] = vertexY;
|
|
vertices[index + 2] = vertexZ;
|
|
// Add normal vector
|
|
normals[index] = normalX;
|
|
normals[index + 1] = normalY;
|
|
normals[index + 2] = normalZ;
|
|
// Get bounding box
|
|
maxX = Math.max(maxX, vertexX);
|
|
maxY = Math.max(maxY, vertexY);
|
|
maxZ = Math.max(maxZ, vertexZ);
|
|
minX = Math.min(minX, vertexX);
|
|
minY = Math.min(minY, vertexY);
|
|
minZ = Math.min(minZ, vertexZ);
|
|
// Aggregate vertex points
|
|
avgX += vertexX;
|
|
avgY += vertexY;
|
|
avgZ += vertexZ;
|
|
// Set color if exists
|
|
if (hasColors) {
|
|
colors[index] = colorR;
|
|
colors[index + 1] = colorG;
|
|
colors[index + 2] = colorB;
|
|
}
|
|
index += 3;
|
|
}
|
|
// Skip the attribute byte count
|
|
reader.readUInt16();
|
|
}
|
|
};
|
|
|
|
// Parse ASCII stl file
|
|
parseASCIISTL = function() {
|
|
var figures, data;
|
|
var index, i, j;
|
|
|
|
// To parse the ASCII STL file, we need to read the data itself.
|
|
// STL file is written as below. We need to remove the structures
|
|
// and extract numbers only.
|
|
// - ASCII STL File Structure:
|
|
// solid name
|
|
// facet normal ni nj nk
|
|
// outer loop
|
|
// vertex v1x v1y v1z
|
|
// vertex v2x v2y v2z
|
|
// vertex v3x v3y v3z
|
|
// endloop
|
|
// endfacet
|
|
// endsolid name
|
|
|
|
data = rawText.replace(/\r/g, "\n"); // make linebreak
|
|
data = data.replace(/(\s)+\1/g, "\n"); // remove first whitespaces of each line
|
|
data = data.replace(/(solid|endsolid)[^.\n]+(\n?)/g, ""); // remove first and last lines
|
|
data = data.replace(/\n/g, " "); // remove linebreaks
|
|
data = data.replace(/facet normal /g, ""); // remove 'facet normal'
|
|
data = data.replace(/outer loop /g, ""); // remove 'outer loop'
|
|
data = data.replace(/vertex /g, ""); // remove 'vertex'
|
|
data = data.replace(/endloop /g, ""); // remove 'endloop'
|
|
data = data.replace(/endfacet(\s?)/g, ""); // remove 'endfacet'
|
|
|
|
figures = data.split(" ");
|
|
facets = (figures.length - 1) / 12;
|
|
index = 0;
|
|
|
|
normals = new Float32Array(facets * 3 * 3);
|
|
vertices = new Float32Array(facets * 3 * 3);
|
|
vertexCount = 0;
|
|
|
|
for (i = 0; i < facets; i++) {
|
|
// Send current progress
|
|
if (i % 100 == 0) {
|
|
workerFacadeMessage({
|
|
type: "progress",
|
|
data: Math.round((i/facets) * 100)
|
|
});
|
|
}
|
|
// Get normal vector
|
|
normalX = figures[i*12];
|
|
normalY = figures[i*12 + 1];
|
|
normalZ = figures[i*12 + 2];
|
|
for (j = 1; j <= 3; j++) {
|
|
// Add vertex vector
|
|
vertexX = figures[i*12 + j*3];
|
|
vertexY = figures[i*12 + j*3 + 1];
|
|
vertexZ = figures[i*12 + j*3 + 2];
|
|
vertices[index] = vertexX;
|
|
vertices[index + 1] = vertexY;
|
|
vertices[index + 2] = vertexZ;
|
|
// Add normal vector
|
|
normals[index] = normalX;
|
|
normals[index + 1] = normalY;
|
|
normals[index + 2] = normalZ;
|
|
// Get bounding box
|
|
maxX = Math.max(maxX, vertexX);
|
|
maxY = Math.max(maxY, vertexY);
|
|
maxZ = Math.max(maxZ, vertexZ);
|
|
minX = Math.min(minX, vertexX);
|
|
minY = Math.min(minY, vertexY);
|
|
minZ = Math.min(minZ, vertexZ);
|
|
// Aggregate vertex points
|
|
avgX += vertexX;
|
|
avgY += vertexY;
|
|
avgZ += vertexZ;
|
|
// Increase index
|
|
index += 3;
|
|
}
|
|
}
|
|
|
|
// Send parsing result
|
|
workerFacadeMessage({
|
|
type: "convert",
|
|
data: data
|
|
});
|
|
};
|
|
|
|
// Check STL File type and parse it.
|
|
if (checkSTLType(reader)) parseBinarySTL();
|
|
else parseASCIISTL();
|
|
|
|
// Get the densest point of object (most promising center point of object)
|
|
avgX = avgX / (facets * 3);
|
|
avgY = avgY / (facets * 3);
|
|
avgZ = avgZ / (facets * 3);
|
|
|
|
var calibrate = (5 < Math.abs((maxX + minX) / 2) && Math.round(Math.abs(avgX)) != 0);
|
|
|
|
// Sometimes objects are placed far from the center of x-axis
|
|
// If object is far from the center of x-axis, shift the position
|
|
if (calibrate) {
|
|
// Report calibration
|
|
workerFacadeMessage({
|
|
type: "message",
|
|
data: "Calibrating object position (X-Delta: " + Math.round(avgX) +
|
|
", Y-Delta: " + Math.round(avgY) + ")"
|
|
});
|
|
// Shift all points to center of screen
|
|
for (i = 0; i < vertices.length/3; i++) {
|
|
vertices[i*3] = vertices[i*3] - avgX;
|
|
vertices[i*3+1] = vertices[i*3+1] - avgY;
|
|
vertices[i*3+2] = vertices[i*3+2] - avgZ;
|
|
}
|
|
}
|
|
|
|
// Send acquired bounding box
|
|
workerFacadeMessage({
|
|
type: "data",
|
|
prop: "bounds",
|
|
data: {
|
|
max: {x: (calibrate ? maxX - avgX : maxX), y: (calibrate ? maxY - avgY : maxY), z: (calibrate ? maxZ - avgZ : maxZ)},
|
|
min: {x: (calibrate ? minX - avgX : minX), y: (calibrate ? minY - avgY : minY), z: (calibrate ? minZ - avgZ : minZ)}
|
|
}
|
|
});
|
|
|
|
// Send acquired center
|
|
workerFacadeMessage({
|
|
type: "data",
|
|
prop: "center",
|
|
data: [avgX, avgY, avgZ]
|
|
});
|
|
|
|
// Send parsing result
|
|
workerFacadeMessage({
|
|
type: "done",
|
|
data: {
|
|
hasColors: hasColors,
|
|
vertices: vertices,
|
|
normals: normals,
|
|
colors: colors,
|
|
alpha: alpha
|
|
}
|
|
});
|
|
};
|
|
|
|
if (typeof(window) === "undefined") {
|
|
onmessage = MadeleineLoader;
|
|
workerFacadeMessage = postMessage;
|
|
} else {
|
|
workerFacadeMessage = WorkerFacade.add("../src/lib/MadeleineLoader.js", MadeleineLoader);
|
|
}
|
|
|
|
// Source: http://threejs.org/examples/js/loaders/STLLoader.js
|
|
// In case DataView is not supported from the browser, create it.
|
|
if (typeof DataView === 'undefined') {
|
|
DataView = function(buffer, byteOffset, byteLength){
|
|
this.buffer = buffer;
|
|
this.byteOffset = byteOffset || 0;
|
|
this.byteLength = byteLength || buffer.byteLength || buffer.length;
|
|
this._isString = typeof buffer === "string";
|
|
}
|
|
DataView.prototype = {
|
|
_getCharCodes:function(buffer,start,length){
|
|
start = start || 0;
|
|
length = length || buffer.length;
|
|
var end = start + length;
|
|
var codes = [];
|
|
for (var i = start; i < end; i++) {
|
|
codes.push(buffer.charCodeAt(i) & 0xff);
|
|
}
|
|
return codes;
|
|
},
|
|
_getBytes: function (length, byteOffset, littleEndian) {
|
|
var result;
|
|
// Handle the lack of endianness
|
|
if (littleEndian === undefined) {
|
|
littleEndian = this._littleEndian;
|
|
}
|
|
// Handle the lack of byteOffset
|
|
if (byteOffset === undefined) {
|
|
byteOffset = this.byteOffset;
|
|
} else {
|
|
byteOffset = this.byteOffset + byteOffset;
|
|
}
|
|
if (length === undefined) {
|
|
length = this.byteLength - byteOffset;
|
|
}
|
|
// Error Checking
|
|
if (typeof byteOffset !== 'number') {
|
|
throw new TypeError('DataView byteOffset is not a number');
|
|
}
|
|
if (length < 0 || byteOffset + length > this.byteLength) {
|
|
throw new Error('DataView length or (byteOffset+length) value is out of bounds');
|
|
}
|
|
if (this.isString){
|
|
result = this._getCharCodes(this.buffer, byteOffset, byteOffset + length);
|
|
} else {
|
|
result = this.buffer.slice(byteOffset, byteOffset + length);
|
|
}
|
|
if (!littleEndian && length > 1) {
|
|
if (!(result instanceof Array)) {
|
|
result = Array.prototype.slice.call(result);
|
|
}
|
|
result.reverse();
|
|
}
|
|
return result;
|
|
},
|
|
// Compatibility functions on a String Buffer
|
|
getFloat64: function (byteOffset, littleEndian) {
|
|
var b = this._getBytes(8, byteOffset, littleEndian),
|
|
sign = 1 - (2 * (b[7] >> 7)),
|
|
exponent = ((((b[7] << 1) & 0xff) << 3) | (b[6] >> 4)) - ((1 << 10) - 1),
|
|
// Binary operators such as | and << operate on 32 bit values, using + and Math.pow(2) instead
|
|
mantissa = ((b[6] & 0x0f) * Math.pow(2, 48)) + (b[5] * Math.pow(2, 40)) + (b[4] * Math.pow(2, 32)) +
|
|
(b[3] * Math.pow(2, 24)) + (b[2] * Math.pow(2, 16)) + (b[1] * Math.pow(2, 8)) + b[0];
|
|
if (exponent === 1024) {
|
|
if (mantissa !== 0) return NaN;
|
|
else return sign * Infinity;
|
|
}
|
|
if (exponent === -1023) { // Denormalized
|
|
return sign * mantissa * Math.pow(2, -1022 - 52);
|
|
}
|
|
return sign * (1 + mantissa * Math.pow(2, -52)) * Math.pow(2, exponent);
|
|
},
|
|
getFloat32: function (byteOffset, littleEndian) {
|
|
var b = this._getBytes(4, byteOffset, littleEndian),
|
|
sign = 1 - (2 * (b[3] >> 7)),
|
|
exponent = (((b[3] << 1) & 0xff) | (b[2] >> 7)) - 127,
|
|
mantissa = ((b[2] & 0x7f) << 16) | (b[1] << 8) | b[0];
|
|
if (exponent === 128) {
|
|
if (mantissa !== 0) return NaN;
|
|
else return sign * Infinity;
|
|
}
|
|
if (exponent === -127) { // Denormalized
|
|
return sign * mantissa * Math.pow(2, -126 - 23);
|
|
}
|
|
return sign * (1 + mantissa * Math.pow(2, -23)) * Math.pow(2, exponent);
|
|
},
|
|
getInt32: function (byteOffset, littleEndian) {
|
|
var b = this._getBytes(4, byteOffset, littleEndian);
|
|
return (b[3] << 24) | (b[2] << 16) | (b[1] << 8) | b[0];
|
|
},
|
|
getUint32: function (byteOffset, littleEndian) {
|
|
return this.getInt32(byteOffset, littleEndian) >>> 0;
|
|
},
|
|
getInt16: function (byteOffset, littleEndian) {
|
|
return (this.getUint16(byteOffset, littleEndian) << 16) >> 16;
|
|
},
|
|
getUint16: function (byteOffset, littleEndian) {
|
|
var b = this._getBytes(2, byteOffset, littleEndian);
|
|
return (b[1] << 8) | b[0];
|
|
},
|
|
getInt8: function (byteOffset) {
|
|
return (this.getUint8(byteOffset) << 24) >> 24;
|
|
},
|
|
getUint8: function (byteOffset) {
|
|
return this._getBytes(1, byteOffset)[0];
|
|
}
|
|
};
|
|
}
|
|
|
|
// Custom Data Reader.
|
|
// Caution: No protection of accessing index over its size limit.
|
|
DataReader = function(binary) {
|
|
this.idx = 0; // in bytes
|
|
this.data = binary;
|
|
this.buffer = null;
|
|
this.reader = new DataView(binary); // binary = arrayBuffer
|
|
this.littleEndian = true;
|
|
|
|
// Helper functions
|
|
this.skip = function(idx) { this.idx += idx };
|
|
this.reset = function() { this.idx = 0 };
|
|
this.getLength = function() { return this.data.byteLength };
|
|
this.getCurrIndex = function() { return this.idx };
|
|
|
|
// Simply get the data at specified index.
|
|
this.getInt8 = function() { return this.reader.getInt8(arguments[0], 1 < arguments.length ? arguments[1] : this.littleEndian) };
|
|
this.getInt16 = function() { return this.reader.getInt16(arguments[0], 1 < arguments.length ? arguments[1] : this.littleEndian) };
|
|
this.getInt32 = function() { return this.reader.getInt32(arguments[0], 1 < arguments.length ? arguments[1] : this.littleEndian) };
|
|
this.getUInt8 = function() { return this.reader.getUint8(arguments[0], 1 < arguments.length ? arguments[1] : this.littleEndian) };
|
|
this.getUInt16 = function() { return this.reader.getUint16(arguments[0], 1 < arguments.length ? arguments[1] : this.littleEndian) };
|
|
this.getUInt32 = function() { return this.reader.getUint32(arguments[0], 1 < arguments.length ? arguments[1] : this.littleEndian) };
|
|
this.getFloat32 = function() { return this.reader.getFloat32(arguments[0], 1 < arguments.length ? arguments[1] : this.littleEndian) };
|
|
this.getFloat64 = function() { return this.reader.getFloat64(arguments[0], 1 < arguments.length ? arguments[1] : this.littleEndian) };
|
|
|
|
// Read the data from the current index.
|
|
// After reading the data, the index moves as much the data read.
|
|
this.readInt8 = function() {
|
|
this.buffer = this.reader.getInt8(this.idx, 0 < arguments.length ? arguments[0] : this.littleEndian);
|
|
this.idx += 1; return this.buffer;
|
|
};
|
|
this.readInt16 = function() {
|
|
this.buffer = this.reader.getInt16(this.idx, 0 < arguments.length ? arguments[0] : this.littleEndian);
|
|
this.idx += 2; return this.buffer;
|
|
};
|
|
this.readInt32 = function() {
|
|
this.buffer = this.reader.getInt32(this.idx, 0 < arguments.length ? arguments[0] : this.littleEndian);
|
|
this.idx += 4; return this.buffer;
|
|
};
|
|
this.readUInt8 = function() {
|
|
this.buffer = this.reader.getUint8(this.idx, 0 < arguments.length ? arguments[0] : this.littleEndian);
|
|
this.idx += 1; return this.buffer;
|
|
};
|
|
this.readUInt16 = function() {
|
|
this.buffer = this.reader.getUint16(this.idx, 0 < arguments.length ? arguments[0] : this.littleEndian);
|
|
this.idx += 2; return this.buffer;
|
|
};
|
|
this.readUInt32 = function() {
|
|
this.buffer = this.reader.getUint32(this.idx, 0 < arguments.length ? arguments[0] : this.littleEndian);
|
|
this.idx += 4; return this.buffer;
|
|
};
|
|
this.readFloat32 = function() {
|
|
this.buffer = this.reader.getFloat32(this.idx, 0 < arguments.length ? arguments[0] : this.littleEndian);
|
|
this.idx += 4; return this.buffer;
|
|
};
|
|
this.readFloat64 = function() {
|
|
this.buffer = this.reader.getFloat64(this.idx, 0 < arguments.length ? arguments[0] : this.littleEndian);
|
|
this.idx += 8; return this.buffer;
|
|
};
|
|
};
|