STLLoader.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. import { BufferAttribute, BufferGeometry, FileLoader, Float32BufferAttribute, Loader, LoaderUtils, Vector3 } from '../lib/three.module.js'
  2. /**
  3. * Description: A THREE loader for STL ASCII files, as created by Solidworks and other CAD programs.
  4. *
  5. * Supports both binary and ASCII encoded files, with automatic detection of type.
  6. *
  7. * The loader returns a non-indexed buffer geometry.
  8. *
  9. * Limitations:
  10. * Binary decoding supports "Magics" color format (http://en.wikipedia.org/wiki/STL_(file_format)#Color_in_binary_STL).
  11. * There is perhaps some question as to how valid it is to always assume little-endian-ness.
  12. * ASCII decoding assumes file is UTF-8.
  13. *
  14. * Usage:
  15. * const loader = new STLLoader();
  16. * loader.load( './models/stl/slotted_disk.stl', function ( geometry ) {
  17. * scene.add( new THREE.Mesh( geometry ) );
  18. * });
  19. *
  20. * For binary STLs geometry might contain colors for vertices. To use it:
  21. * // use the same code to load STL as above
  22. * if (geometry.hasColors) {
  23. * material = new THREE.MeshPhongMaterial({ opacity: geometry.alpha, vertexColors: true });
  24. * } else { .... }
  25. * const mesh = new THREE.Mesh( geometry, material );
  26. *
  27. * For ASCII STLs containing multiple solids, each solid is assigned to a different group.
  28. * Groups can be used to assign a different color by defining an array of materials with the same length of
  29. * geometry.groups and passing it to the Mesh constructor:
  30. *
  31. * const mesh = new THREE.Mesh( geometry, material );
  32. *
  33. * For example:
  34. *
  35. * const materials = [];
  36. * const nGeometryGroups = geometry.groups.length;
  37. *
  38. * const colorMap = ...; // Some logic to index colors.
  39. *
  40. * for (let i = 0; i < nGeometryGroups; i++) {
  41. *
  42. * const material = new THREE.MeshPhongMaterial({
  43. * color: colorMap[i],
  44. * wireframe: false
  45. * });
  46. *
  47. * }
  48. *
  49. * materials.push(material);
  50. * const mesh = new THREE.Mesh(geometry, materials);
  51. */
  52. class STLLoader extends Loader {
  53. constructor(manager) {
  54. super(manager)
  55. }
  56. load(url, onLoad, onProgress, onError) {
  57. const scope = this
  58. const loader = new FileLoader(this.manager)
  59. loader.setPath(this.path)
  60. loader.setResponseType('arraybuffer')
  61. loader.setRequestHeader(this.requestHeader)
  62. loader.setWithCredentials(this.withCredentials)
  63. loader.load(
  64. url,
  65. function(text) {
  66. try {
  67. onLoad(scope.parse(text))
  68. } catch (e) {
  69. if (onError) {
  70. onError(e)
  71. } else {
  72. console.error(e)
  73. }
  74. scope.manager.itemError(url)
  75. }
  76. },
  77. onProgress,
  78. onError
  79. )
  80. }
  81. parse(data) {
  82. function isBinary(data) {
  83. const reader = new DataView(data)
  84. const face_size = (32 / 8) * 3 + (32 / 8) * 3 * 3 + 16 / 8
  85. const n_faces = reader.getUint32(80, true)
  86. const expect = 80 + 32 / 8 + n_faces * face_size
  87. if (expect === reader.byteLength) {
  88. return true
  89. }
  90. // An ASCII STL data must begin with 'solid ' as the first six bytes.
  91. // However, ASCII STLs lacking the SPACE after the 'd' are known to be
  92. // plentiful. So, check the first 5 bytes for 'solid'.
  93. // Several encodings, such as UTF-8, precede the text with up to 5 bytes:
  94. // https://en.wikipedia.org/wiki/Byte_order_mark#Byte_order_marks_by_encoding
  95. // Search for "solid" to start anywhere after those prefixes.
  96. // US-ASCII ordinal values for 's', 'o', 'l', 'i', 'd'
  97. const solid = [115, 111, 108, 105, 100]
  98. for (let off = 0; off < 5; off++) {
  99. // If "solid" text is matched to the current offset, declare it to be an ASCII STL.
  100. if (matchDataViewAt(solid, reader, off)) return false
  101. }
  102. // Couldn't find "solid" text at the beginning; it is binary STL.
  103. return true
  104. }
  105. function matchDataViewAt(query, reader, offset) {
  106. // Check if each byte in query matches the corresponding byte from the current offset
  107. for (let i = 0, il = query.length; i < il; i++) {
  108. if (query[i] !== reader.getUint8(offset + i, false)) return false
  109. }
  110. return true
  111. }
  112. function parseBinary(data) {
  113. const reader = new DataView(data)
  114. const faces = reader.getUint32(80, true)
  115. let r,
  116. g,
  117. b,
  118. hasColors = false,
  119. colors
  120. let defaultR, defaultG, defaultB, alpha
  121. // process STL header
  122. // check for default color in header ("COLOR=rgba" sequence).
  123. for (let index = 0; index < 80 - 10; index++) {
  124. if (reader.getUint32(index, false) == 0x434f4c4f /*COLO*/ && reader.getUint8(index + 4) == 0x52 /*'R'*/ && reader.getUint8(index + 5) == 0x3d /*'='*/) {
  125. hasColors = true
  126. colors = new Float32Array(faces * 3 * 3)
  127. defaultR = reader.getUint8(index + 6) / 255
  128. defaultG = reader.getUint8(index + 7) / 255
  129. defaultB = reader.getUint8(index + 8) / 255
  130. alpha = reader.getUint8(index + 9) / 255
  131. }
  132. }
  133. const dataOffset = 84
  134. const faceLength = 12 * 4 + 2
  135. const geometry = new BufferGeometry()
  136. const vertices = new Float32Array(faces * 3 * 3)
  137. const normals = new Float32Array(faces * 3 * 3)
  138. for (let face = 0; face < faces; face++) {
  139. const start = dataOffset + face * faceLength
  140. const normalX = reader.getFloat32(start, true)
  141. const normalY = reader.getFloat32(start + 4, true)
  142. const normalZ = reader.getFloat32(start + 8, true)
  143. if (hasColors) {
  144. const packedColor = reader.getUint16(start + 48, true)
  145. if ((packedColor & 0x8000) === 0) {
  146. // facet has its own unique color
  147. r = (packedColor & 0x1f) / 31
  148. g = ((packedColor >> 5) & 0x1f) / 31
  149. b = ((packedColor >> 10) & 0x1f) / 31
  150. } else {
  151. r = defaultR
  152. g = defaultG
  153. b = defaultB
  154. }
  155. }
  156. for (let i = 1; i <= 3; i++) {
  157. const vertexstart = start + i * 12
  158. const componentIdx = face * 3 * 3 + (i - 1) * 3
  159. vertices[componentIdx] = reader.getFloat32(vertexstart, true)
  160. vertices[componentIdx + 1] = reader.getFloat32(vertexstart + 4, true)
  161. vertices[componentIdx + 2] = reader.getFloat32(vertexstart + 8, true)
  162. normals[componentIdx] = normalX
  163. normals[componentIdx + 1] = normalY
  164. normals[componentIdx + 2] = normalZ
  165. if (hasColors) {
  166. colors[componentIdx] = r
  167. colors[componentIdx + 1] = g
  168. colors[componentIdx + 2] = b
  169. }
  170. }
  171. }
  172. geometry.setAttribute('position', new BufferAttribute(vertices, 3))
  173. geometry.setAttribute('normal', new BufferAttribute(normals, 3))
  174. if (hasColors) {
  175. geometry.setAttribute('color', new BufferAttribute(colors, 3))
  176. geometry.hasColors = true
  177. geometry.alpha = alpha
  178. }
  179. return geometry
  180. }
  181. function parseASCII(data) {
  182. const geometry = new BufferGeometry()
  183. const patternSolid = /solid([\s\S]*?)endsolid/g
  184. const patternFace = /facet([\s\S]*?)endfacet/g
  185. let faceCounter = 0
  186. const patternFloat = /[\s]+([+-]?(?:\d*)(?:\.\d*)?(?:[eE][+-]?\d+)?)/.source
  187. const patternVertex = new RegExp('vertex' + patternFloat + patternFloat + patternFloat, 'g')
  188. const patternNormal = new RegExp('normal' + patternFloat + patternFloat + patternFloat, 'g')
  189. const vertices = []
  190. const normals = []
  191. const normal = new Vector3()
  192. let result
  193. let groupCount = 0
  194. let startVertex = 0
  195. let endVertex = 0
  196. while ((result = patternSolid.exec(data)) !== null) {
  197. startVertex = endVertex
  198. const solid = result[0]
  199. while ((result = patternFace.exec(solid)) !== null) {
  200. let vertexCountPerFace = 0
  201. let normalCountPerFace = 0
  202. const text = result[0]
  203. while ((result = patternNormal.exec(text)) !== null) {
  204. normal.x = parseFloat(result[1])
  205. normal.y = parseFloat(result[2])
  206. normal.z = parseFloat(result[3])
  207. normalCountPerFace++
  208. }
  209. while ((result = patternVertex.exec(text)) !== null) {
  210. vertices.push(parseFloat(result[1]), parseFloat(result[2]), parseFloat(result[3]))
  211. normals.push(normal.x, normal.y, normal.z)
  212. vertexCountPerFace++
  213. endVertex++
  214. }
  215. // every face have to own ONE valid normal
  216. if (normalCountPerFace !== 1) {
  217. console.error("THREE.STLLoader: Something isn't right with the normal of face number " + faceCounter)
  218. }
  219. // each face have to own THREE valid vertices
  220. if (vertexCountPerFace !== 3) {
  221. console.error("THREE.STLLoader: Something isn't right with the vertices of face number " + faceCounter)
  222. }
  223. faceCounter++
  224. }
  225. const start = startVertex
  226. const count = endVertex - startVertex
  227. geometry.addGroup(start, count, groupCount)
  228. groupCount++
  229. }
  230. geometry.setAttribute('position', new Float32BufferAttribute(vertices, 3))
  231. geometry.setAttribute('normal', new Float32BufferAttribute(normals, 3))
  232. return geometry
  233. }
  234. function ensureString(buffer) {
  235. if (typeof buffer !== 'string') {
  236. return LoaderUtils.decodeText(new Uint8Array(buffer))
  237. }
  238. return buffer
  239. }
  240. function ensureBinary(buffer) {
  241. if (typeof buffer === 'string') {
  242. const array_buffer = new Uint8Array(buffer.length)
  243. for (let i = 0; i < buffer.length; i++) {
  244. array_buffer[i] = buffer.charCodeAt(i) & 0xff // implicitly assumes little-endian
  245. }
  246. return array_buffer.buffer || array_buffer
  247. } else {
  248. return buffer
  249. }
  250. }
  251. // start
  252. const binData = ensureBinary(data)
  253. return isBinary(binData) ? parseBinary(binData) : parseASCII(ensureString(data))
  254. }
  255. }
  256. export { STLLoader }