|
@@ -0,0 +1,361 @@
|
|
|
|
+(function () {
|
|
|
|
+ /**
|
|
|
|
+ * Description: A THREE loader for STL ASCII files, as created by Solidworks and other CAD programs.
|
|
|
|
+ *
|
|
|
|
+ * Supports both binary and ASCII encoded files, with automatic detection of type.
|
|
|
|
+ *
|
|
|
|
+ * The loader returns a non-indexed buffer geometry.
|
|
|
|
+ *
|
|
|
|
+ * Limitations:
|
|
|
|
+ * Binary decoding supports "Magics" color format (http://en.wikipedia.org/wiki/STL_(file_format)#Color_in_binary_STL).
|
|
|
|
+ * There is perhaps some question as to how valid it is to always assume little-endian-ness.
|
|
|
|
+ * ASCII decoding assumes file is UTF-8.
|
|
|
|
+ *
|
|
|
|
+ * Usage:
|
|
|
|
+ * const loader = new STLLoader();
|
|
|
|
+ * loader.load( './models/stl/slotted_disk.stl', function ( geometry ) {
|
|
|
|
+ * scene.add( new THREE.Mesh( geometry ) );
|
|
|
|
+ * });
|
|
|
|
+ *
|
|
|
|
+ * For binary STLs geometry might contain colors for vertices. To use it:
|
|
|
|
+ * // use the same code to load STL as above
|
|
|
|
+ * if (geometry.hasColors) {
|
|
|
|
+ * material = new THREE.MeshPhongMaterial({ opacity: geometry.alpha, vertexColors: true });
|
|
|
|
+ * } else { .... }
|
|
|
|
+ * const mesh = new THREE.Mesh( geometry, material );
|
|
|
|
+ *
|
|
|
|
+ * For ASCII STLs containing multiple solids, each solid is assigned to a different group.
|
|
|
|
+ * Groups can be used to assign a different color by defining an array of materials with the same length of
|
|
|
|
+ * geometry.groups and passing it to the Mesh constructor:
|
|
|
|
+ *
|
|
|
|
+ * const mesh = new THREE.Mesh( geometry, material );
|
|
|
|
+ *
|
|
|
|
+ * For example:
|
|
|
|
+ *
|
|
|
|
+ * const materials = [];
|
|
|
|
+ * const nGeometryGroups = geometry.groups.length;
|
|
|
|
+ *
|
|
|
|
+ * const colorMap = ...; // Some logic to index colors.
|
|
|
|
+ *
|
|
|
|
+ * for (let i = 0; i < nGeometryGroups; i++) {
|
|
|
|
+ *
|
|
|
|
+ * const material = new THREE.MeshPhongMaterial({
|
|
|
|
+ * color: colorMap[i],
|
|
|
|
+ * wireframe: false
|
|
|
|
+ * });
|
|
|
|
+ *
|
|
|
|
+ * }
|
|
|
|
+ *
|
|
|
|
+ * materials.push(material);
|
|
|
|
+ * const mesh = new THREE.Mesh(geometry, materials);
|
|
|
|
+ */
|
|
|
|
+
|
|
|
|
+ class STLLoader extends THREE.Loader {
|
|
|
|
+ constructor(manager) {
|
|
|
|
+ super(manager);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ load(url, onLoad, onProgress, onError) {
|
|
|
|
+ const scope = this;
|
|
|
|
+
|
|
|
|
+ const loader = new THREE.FileLoader(this.manager);
|
|
|
|
+ loader.setPath(this.path);
|
|
|
|
+ loader.setResponseType("arraybuffer");
|
|
|
|
+ loader.setRequestHeader(this.requestHeader);
|
|
|
|
+ loader.setWithCredentials(this.withCredentials);
|
|
|
|
+
|
|
|
|
+ loader.load(
|
|
|
|
+ url,
|
|
|
|
+ function (text) {
|
|
|
|
+ try {
|
|
|
|
+ onLoad(scope.parse(text));
|
|
|
|
+ } catch (e) {
|
|
|
|
+ if (onError) {
|
|
|
|
+ onError(e);
|
|
|
|
+ } else {
|
|
|
|
+ console.error(e);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ scope.manager.itemError(url);
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ onProgress,
|
|
|
|
+ onError
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ parse(data) {
|
|
|
|
+ function isBinary(data) {
|
|
|
|
+ const reader = new DataView(data);
|
|
|
|
+ const face_size = (32 / 8) * 3 + (32 / 8) * 3 * 3 + 16 / 8;
|
|
|
|
+ const n_faces = reader.getUint32(80, true);
|
|
|
|
+ const expect = 80 + 32 / 8 + n_faces * face_size;
|
|
|
|
+
|
|
|
|
+ if (expect === reader.byteLength) {
|
|
|
|
+ return true;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // An ASCII STL data must begin with 'solid ' as the first six bytes.
|
|
|
|
+ // However, ASCII STLs lacking the SPACE after the 'd' are known to be
|
|
|
|
+ // plentiful. So, check the first 5 bytes for 'solid'.
|
|
|
|
+
|
|
|
|
+ // Several encodings, such as UTF-8, precede the text with up to 5 bytes:
|
|
|
|
+ // https://en.wikipedia.org/wiki/Byte_order_mark#Byte_order_marks_by_encoding
|
|
|
|
+ // Search for "solid" to start anywhere after those prefixes.
|
|
|
|
+
|
|
|
|
+ // US-ASCII ordinal values for 's', 'o', 'l', 'i', 'd'
|
|
|
|
+
|
|
|
|
+ const solid = [115, 111, 108, 105, 100];
|
|
|
|
+
|
|
|
|
+ for (let off = 0; off < 5; off++) {
|
|
|
|
+ // If "solid" text is matched to the current offset, declare it to be an ASCII STL.
|
|
|
|
+
|
|
|
|
+ if (matchDataViewAt(solid, reader, off)) return false;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Couldn't find "solid" text at the beginning; it is binary STL.
|
|
|
|
+
|
|
|
|
+ return true;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ function matchDataViewAt(query, reader, offset) {
|
|
|
|
+ // Check if each byte in query matches the corresponding byte from the current offset
|
|
|
|
+
|
|
|
|
+ for (let i = 0, il = query.length; i < il; i++) {
|
|
|
|
+ if (query[i] !== reader.getUint8(offset + i, false)) return false;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return true;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ function parseBinary(data) {
|
|
|
|
+ const reader = new DataView(data);
|
|
|
|
+ const faces = reader.getUint32(80, true);
|
|
|
|
+
|
|
|
|
+ let r,
|
|
|
|
+ g,
|
|
|
|
+ b,
|
|
|
|
+ hasColors = false,
|
|
|
|
+ colors;
|
|
|
|
+ let defaultR, defaultG, defaultB, alpha;
|
|
|
|
+
|
|
|
|
+ // process STL header
|
|
|
|
+ // check for default color in header ("COLOR=rgba" sequence).
|
|
|
|
+
|
|
|
|
+ for (let index = 0; index < 80 - 10; index++) {
|
|
|
|
+ if (
|
|
|
|
+ reader.getUint32(index, false) == 0x434f4c4f /*COLO*/ &&
|
|
|
|
+ reader.getUint8(index + 4) == 0x52 /*'R'*/ &&
|
|
|
|
+ reader.getUint8(index + 5) == 0x3d /*'='*/
|
|
|
|
+ ) {
|
|
|
|
+ hasColors = true;
|
|
|
|
+ colors = new Float32Array(faces * 3 * 3);
|
|
|
|
+
|
|
|
|
+ defaultR = reader.getUint8(index + 6) / 255;
|
|
|
|
+ defaultG = reader.getUint8(index + 7) / 255;
|
|
|
|
+ defaultB = reader.getUint8(index + 8) / 255;
|
|
|
|
+ alpha = reader.getUint8(index + 9) / 255;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ const dataOffset = 84;
|
|
|
|
+ const faceLength = 12 * 4 + 2;
|
|
|
|
+
|
|
|
|
+ const geometry = new THREE.BufferGeometry();
|
|
|
|
+
|
|
|
|
+ const vertices = new Float32Array(faces * 3 * 3);
|
|
|
|
+ const normals = new Float32Array(faces * 3 * 3);
|
|
|
|
+
|
|
|
|
+ for (let face = 0; face < faces; face++) {
|
|
|
|
+ const start = dataOffset + face * faceLength;
|
|
|
|
+ const normalX = reader.getFloat32(start, true);
|
|
|
|
+ const normalY = reader.getFloat32(start + 4, true);
|
|
|
|
+ const normalZ = reader.getFloat32(start + 8, true);
|
|
|
|
+
|
|
|
|
+ if (hasColors) {
|
|
|
|
+ const packedColor = reader.getUint16(start + 48, true);
|
|
|
|
+
|
|
|
|
+ if ((packedColor & 0x8000) === 0) {
|
|
|
|
+ // facet has its own unique color
|
|
|
|
+
|
|
|
|
+ r = (packedColor & 0x1f) / 31;
|
|
|
|
+ g = ((packedColor >> 5) & 0x1f) / 31;
|
|
|
|
+ b = ((packedColor >> 10) & 0x1f) / 31;
|
|
|
|
+ } else {
|
|
|
|
+ r = defaultR;
|
|
|
|
+ g = defaultG;
|
|
|
|
+ b = defaultB;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ for (let i = 1; i <= 3; i++) {
|
|
|
|
+ const vertexstart = start + i * 12;
|
|
|
|
+ const componentIdx = face * 3 * 3 + (i - 1) * 3;
|
|
|
|
+
|
|
|
|
+ vertices[componentIdx] = reader.getFloat32(vertexstart, true);
|
|
|
|
+ vertices[componentIdx + 1] = reader.getFloat32(
|
|
|
|
+ vertexstart + 4,
|
|
|
|
+ true
|
|
|
|
+ );
|
|
|
|
+ vertices[componentIdx + 2] = reader.getFloat32(
|
|
|
|
+ vertexstart + 8,
|
|
|
|
+ true
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ normals[componentIdx] = normalX;
|
|
|
|
+ normals[componentIdx + 1] = normalY;
|
|
|
|
+ normals[componentIdx + 2] = normalZ;
|
|
|
|
+
|
|
|
|
+ if (hasColors) {
|
|
|
|
+ colors[componentIdx] = r;
|
|
|
|
+ colors[componentIdx + 1] = g;
|
|
|
|
+ colors[componentIdx + 2] = b;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ geometry.setAttribute(
|
|
|
|
+ "position",
|
|
|
|
+ new THREE.BufferAttribute(vertices, 3)
|
|
|
|
+ );
|
|
|
|
+ geometry.setAttribute("normal", new THREE.BufferAttribute(normals, 3));
|
|
|
|
+
|
|
|
|
+ if (hasColors) {
|
|
|
|
+ geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
|
|
|
|
+ geometry.hasColors = true;
|
|
|
|
+ geometry.alpha = alpha;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return geometry;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ function parseASCII(data) {
|
|
|
|
+ const geometry = new THREE.BufferGeometry();
|
|
|
|
+ const patternSolid = /solid([\s\S]*?)endsolid/g;
|
|
|
|
+ const patternFace = /facet([\s\S]*?)endfacet/g;
|
|
|
|
+ let faceCounter = 0;
|
|
|
|
+
|
|
|
|
+ const patternFloat = /[\s]+([+-]?(?:\d*)(?:\.\d*)?(?:[eE][+-]?\d+)?)/
|
|
|
|
+ .source;
|
|
|
|
+ const patternVertex = new RegExp(
|
|
|
|
+ "vertex" + patternFloat + patternFloat + patternFloat,
|
|
|
|
+ "g"
|
|
|
|
+ );
|
|
|
|
+ const patternNormal = new RegExp(
|
|
|
|
+ "normal" + patternFloat + patternFloat + patternFloat,
|
|
|
|
+ "g"
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ const vertices = [];
|
|
|
|
+ const normals = [];
|
|
|
|
+
|
|
|
|
+ const normal = new Vector3();
|
|
|
|
+
|
|
|
|
+ let result;
|
|
|
|
+
|
|
|
|
+ let groupCount = 0;
|
|
|
|
+ let startVertex = 0;
|
|
|
|
+ let endVertex = 0;
|
|
|
|
+
|
|
|
|
+ while ((result = patternSolid.exec(data)) !== null) {
|
|
|
|
+ startVertex = endVertex;
|
|
|
|
+
|
|
|
|
+ const solid = result[0];
|
|
|
|
+
|
|
|
|
+ while ((result = patternFace.exec(solid)) !== null) {
|
|
|
|
+ let vertexCountPerFace = 0;
|
|
|
|
+ let normalCountPerFace = 0;
|
|
|
|
+
|
|
|
|
+ const text = result[0];
|
|
|
|
+
|
|
|
|
+ while ((result = patternNormal.exec(text)) !== null) {
|
|
|
|
+ normal.x = parseFloat(result[1]);
|
|
|
|
+ normal.y = parseFloat(result[2]);
|
|
|
|
+ normal.z = parseFloat(result[3]);
|
|
|
|
+ normalCountPerFace++;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ while ((result = patternVertex.exec(text)) !== null) {
|
|
|
|
+ vertices.push(
|
|
|
|
+ parseFloat(result[1]),
|
|
|
|
+ parseFloat(result[2]),
|
|
|
|
+ parseFloat(result[3])
|
|
|
|
+ );
|
|
|
|
+ normals.push(normal.x, normal.y, normal.z);
|
|
|
|
+ vertexCountPerFace++;
|
|
|
|
+ endVertex++;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // every face have to own ONE valid normal
|
|
|
|
+
|
|
|
|
+ if (normalCountPerFace !== 1) {
|
|
|
|
+ console.error(
|
|
|
|
+ "THREE.STLLoader: Something isn't right with the normal of face number " +
|
|
|
|
+ faceCounter
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // each face have to own THREE valid vertices
|
|
|
|
+
|
|
|
|
+ if (vertexCountPerFace !== 3) {
|
|
|
|
+ console.error(
|
|
|
|
+ "THREE.STLLoader: Something isn't right with the vertices of face number " +
|
|
|
|
+ faceCounter
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ faceCounter++;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ const start = startVertex;
|
|
|
|
+ const count = endVertex - startVertex;
|
|
|
|
+
|
|
|
|
+ geometry.addGroup(start, count, groupCount);
|
|
|
|
+ groupCount++;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ geometry.setAttribute(
|
|
|
|
+ "position",
|
|
|
|
+ new THREE.Float32BufferAttribute(vertices, 3)
|
|
|
|
+ );
|
|
|
|
+ geometry.setAttribute(
|
|
|
|
+ "normal",
|
|
|
|
+ new THREE.Float32BufferAttribute(normals, 3)
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ return geometry;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ function ensureString(buffer) {
|
|
|
|
+ if (typeof buffer !== "string") {
|
|
|
|
+ return LoaderUtils.decodeText(new Uint8Array(buffer));
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return buffer;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ function ensureBinary(buffer) {
|
|
|
|
+ if (typeof buffer === "string") {
|
|
|
|
+ const array_buffer = new Uint8Array(buffer.length);
|
|
|
|
+ for (let i = 0; i < buffer.length; i++) {
|
|
|
|
+ array_buffer[i] = buffer.charCodeAt(i) & 0xff; // implicitly assumes little-endian
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return array_buffer.buffer || array_buffer;
|
|
|
|
+ } else {
|
|
|
|
+ return buffer;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // start
|
|
|
|
+
|
|
|
|
+ const binData = ensureBinary(data);
|
|
|
|
+
|
|
|
|
+ return isBinary(binData)
|
|
|
|
+ ? parseBinary(binData)
|
|
|
|
+ : parseASCII(ensureString(data));
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ THREE.STLLoader = STLLoader;
|
|
|
|
+})();
|