123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323 |
- 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 }
|