import { BufferAttribute, BufferGeometry, FileLoader, Float32BufferAttribute, Loader, LoaderUtils, Vector3 } from '../lib/three.module.js' /** * 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 Loader { constructor(manager) { super(manager) } load(url, onLoad, onProgress, onError) { const scope = this const loader = new 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 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 BufferAttribute(vertices, 3)) geometry.setAttribute('normal', new BufferAttribute(normals, 3)) if (hasColors) { geometry.setAttribute('color', new BufferAttribute(colors, 3)) geometry.hasColors = true geometry.alpha = alpha } return geometry } function parseASCII(data) { const geometry = new 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 Float32BufferAttribute(vertices, 3)) geometry.setAttribute('normal', new 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)) } } export { STLLoader }