split.html 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. <html>
  2. <title>Split STL mesh</title>
  3. <body>
  4. <p>This is a simple piece of javascript (licensed under the MIT license) by
  5. <a href="http://www.thingiverse.com/arpruss/">Alexander Pruss</a> that splits an STL mesh file that contains multiple parts into
  6. those multiple parts.</p>
  7. <p>This may well fail if the mesh is defective and has points that are meant to be coincident
  8. but are merely close together.</p>
  9. <p>All the processing is done in your browser--your STL is not uploaded to any server.</p>
  10. <p><b>Warning:</b> If an object has a cavity surrounded by material on all sides, you will get one mesh for the outer surface
  11. and an inside-out mesh for the cavity. Solving this problem would require significant coding.</p>
  12. <input type="file" id="file" name="file" />
  13. <ul id="console">
  14. </ul>
  15. <p id="progress">
  16. </p>
  17. <script>
  18. var TRIANGLE_SIZE = (3+3*3)*4+2;
  19. var ASCII_MODE = true;
  20. var allSplitMeshes = [];
  21. var baseFilaname = "";
  22. var splitMeshIndex;
  23. var meshes;
  24. function getString(view, position, length) {
  25. var out = "";
  26. for (var i=0; i<length; i++) {
  27. var b = view.getUint8(position + i);
  28. out += String.fromCharCode(b);
  29. }
  30. return out;
  31. }
  32. // For hashing during splitting, ASCII mode is used, where vectors are stored
  33. // as strings. From a binary STL, the vectors are stored as hex strings, and
  34. // from an ASCII STL, they are stored as directly extracted ASCII.
  35. function getVector(view, position) {
  36. if (ASCII_MODE) {
  37. function fixZero(a) {
  38. return a == 0x80000000 ? 0 : a;
  39. }
  40. x = fixZero(view.getUint32(position, true));
  41. y = fixZero(view.getUint32(position+4, true));
  42. z = fixZero(view.getUint32(position+8, true));
  43. return x.toString(16)+":"+y.toString(16)+":"+z.toString(16);
  44. }
  45. x = view.getFloat32(position, true);
  46. y = view.getFloat32(position+4, true);
  47. z = view.getFloat32(position+8, true);
  48. return [x,y,z]; // x.toString()+","+y.toString()+","+z.toString(); //[x,y,z];
  49. }
  50. function parseVector(vector) {
  51. if (typeof vector === "string") {
  52. var data = vector.split(":");
  53. var buf = new ArrayBuffer(4);
  54. var view = new DataView(buf);
  55. function parse(s) {
  56. view.setUint32(0, parseInt(s,16));
  57. return view.getFloat32(0);
  58. }
  59. return [parse(data[0]),parse(data[1]),parse(data[2])];
  60. }
  61. else {
  62. return vector;
  63. }
  64. }
  65. function setVector(view, position, vector) {
  66. if (typeof vector === "string") {
  67. var data = vector.split(":");
  68. view.setUint32(position, parseInt(data[0],16), true);
  69. view.setUint32(position+4, parseInt(data[1],16), true);
  70. view.setUint32(position+8, parseInt(data[2],16), true);
  71. }
  72. else {
  73. view.setFloat32(position, vector[0], true);
  74. view.setFloat32(position+4, vector[1], true);
  75. view.setFloat32(position+8, vector[2], true);
  76. }
  77. }
  78. function getVectorFromText(line, position) {
  79. var data = line.substr(position).split(/[\s,]+/);
  80. if (data.length < 3)
  81. throw 'Invalid vector';
  82. if (ASCII_MODE) {
  83. var buf = new ArrayBuffer(4);
  84. var view = new DataView(buf);
  85. function toHex32(number) {
  86. view.setFloat32(0, parseFloat(number));
  87. return view.getUint32(0).toString(16);
  88. }
  89. return toHex32(data[0])+":"+toHex32(data[1])+":"+toHex32(data[2]);
  90. }
  91. return [parseFloat(data[0]), parseFloat(data[1]), parseFloat(data[2])];
  92. }
  93. function message(text) {
  94. document.getElementById('console').innerHTML += '<li>'+text+'</li>';
  95. }
  96. function getASCIISTL(text) {
  97. var triangles = [];
  98. var triangle = [];
  99. var normal = [0,0,0];
  100. var lines = text.split(/[\r\n]+/);
  101. message(lines.length+" lines of data");
  102. for (var i = 0 ; i < lines.length ; i++ ) {
  103. l = lines[i].trim().toLowerCase();
  104. if (l == 'endfacet') {
  105. if (triangle.length != 3)
  106. throw 'invalid triangle';
  107. triangles.push([triangle[0],triangle[1],triangle[2],normal]);
  108. triangle = [];
  109. normal = [0,0,0];
  110. }
  111. else if (l.startsWith('facet')) {
  112. if (l.length > 6) {
  113. if (l.substr(6).startsWith('normal'))
  114. normal = getVectorFromText(l, 13);
  115. }
  116. }
  117. else if (l.startsWith('vertex')) {
  118. if (l.length > 7)
  119. triangle.push(getVectorFromText(l, 7));
  120. }
  121. }
  122. message(String(triangles.length) + " triangles");
  123. return triangles;
  124. }
  125. function getBinarySTL(view) {
  126. var triangles = [];
  127. var numTriangles = view.getUint32(80, true);
  128. message(String(numTriangles) + " triangles");
  129. for (var i = 0 ; i < numTriangles ; i++) {
  130. position = 84 + TRIANGLE_SIZE*i;
  131. normal = getVector(view, position);
  132. v1 = getVector(view, position+12);
  133. v2 = getVector(view, position+12*2);
  134. v3 = getVector(view, position+12*3);
  135. triangles.push( [v1,v2,v3,normal] );
  136. }
  137. return triangles;
  138. }
  139. function meshSelection(meshes) {
  140. message(String(meshes.length)+" meshes extracted");
  141. out = "<p>Download individual part meshes or combine them with checkmarks:<br/>";
  142. allSplitMeshes = [];
  143. for (var i=0; i<meshes.length; i++) {
  144. allSplitMeshes.push(meshes[i].triangles);
  145. out += "<input type='checkbox' id='mesh"+i+"'/><a href='#' onclick='downloadMesh(["+i+"]);'>mesh part "+(i+1)+"</a> "+describeBounds(meshes[i].bounds)+"<br/>";
  146. }
  147. out += "<button onclick='downloadMeshCombo();'>Download combination</button></p>";
  148. document.getElementById('progress').innerHTML = out;
  149. }
  150. function downloadMeshCombo() {
  151. list = [];
  152. for (var i=0; i<allSplitMeshes.length; i++) {
  153. console.log(document.getElementById('mesh'+i));
  154. if (document.getElementById('mesh'+i).checked)
  155. list.push(i);
  156. }
  157. if (list.length == 0)
  158. return;
  159. downloadMesh(list);
  160. }
  161. function splitMesh(triangles) {
  162. meshes = [];
  163. splitMeshIndex = 0;
  164. function process() {
  165. var i = splitMeshIndex;
  166. var t0 = Date.now();
  167. document.getElementById('progress').innerHTML = "Splitting "+(i/triangles.length*100).toFixed(1)+'% done ('+(meshes.length)+' parts found)';
  168. for (; i<triangles.length; i++) {
  169. var t = triangles[i];
  170. var matches = [];
  171. for (var j = 0 ; j < 3 ; j++) {
  172. for (var k = 0 ; k < meshes.length; k++) {
  173. if (matches.indexOf(k) == -1 && t[j] in meshes[k].points) {
  174. matches.push(k);
  175. }
  176. }
  177. }
  178. matches.sort((x,y)=>(x<y ? -1 : (x>y ? 1 : 0)));
  179. var m;
  180. if (matches.length == 0) {
  181. m = {points:{}, triangles:[]};
  182. meshes.push(m);
  183. }
  184. else {
  185. m = meshes[matches[0]];
  186. for (var j = matches.length - 1 ; j >= 1 ; j--) {
  187. mm = meshes[matches[j]];
  188. for (var key in mm.points) {
  189. if (mm.points.hasOwnProperty(key))
  190. m.points[key] = true;
  191. }
  192. for (var k = 0 ; k < mm.triangles.length; k++) {
  193. m.triangles.push(mm.triangles[k]);
  194. }
  195. meshes.splice(matches[j], 1);
  196. }
  197. }
  198. for (var k = 0 ; k < 3 ; k++) {
  199. m.points[t[k]] = true;
  200. }
  201. m.triangles.push(t);
  202. if (Date.now() >= t0 + 500) {
  203. setTimeout(process, 0);
  204. splitMeshIndex = i+1;
  205. return;
  206. }
  207. }
  208. for (var i = 0 ; i < meshes.length ; i++) {
  209. var bounds = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY,
  210. Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY ];
  211. for (var key in meshes[i].points) {
  212. if (meshes[i].points.hasOwnProperty(key)) {
  213. var v = parseVector(key);
  214. for (var k=0; k<3; k++) {
  215. bounds[k] = Math.min(bounds[k], v[k]);
  216. bounds[3+k] = Math.max(bounds[3+k], v[k]);
  217. }
  218. }
  219. }
  220. meshes[i].bounds = bounds;
  221. delete meshes[i].points;
  222. }
  223. function compareByBounds(m,mm) {
  224. for (var i=0; i<6; i++) {
  225. if (m.bounds[i] < mm.bounds[i])
  226. return -1;
  227. else if (mm.bounds[i] < m.bounds[i])
  228. return 1;
  229. }
  230. return 0;
  231. }
  232. meshes.sort(compareByBounds);
  233. document.getElementById('progress').innerHTML = '';
  234. document.getElementById('file').disabled = false;
  235. if (meshes.length == 1) {
  236. message("No splitting done: Only one mesh in file.");
  237. }
  238. else if (meshes.length == 0) {
  239. message("No mesh found in file.");
  240. }
  241. else {
  242. meshSelection(meshes);
  243. }
  244. meshes = [];
  245. }
  246. process();
  247. }
  248. function downloadBlob(name,blob) {
  249. var link = document.createElement('a');
  250. document.body.appendChild(link);
  251. link.download = name;
  252. link.href = window.URL.createObjectURL(blob);
  253. link.onclick = function(e) {
  254. setTimeout(function() {
  255. window.URL.revokeObjectURL(link.href);
  256. }, 1600);
  257. };
  258. link.click();
  259. try {
  260. link.remove();
  261. }
  262. catch(err) {}
  263. try {
  264. document.body.removeChild(link);
  265. }
  266. catch(err) {}
  267. }
  268. function makeMeshByteArray(triangleLists) {
  269. var totalTriangles = 0;
  270. for (var i=0; i<triangleLists.length; i++)
  271. totalTriangles += triangleLists[i].length;
  272. var data = new ArrayBuffer(84 + totalTriangles * TRIANGLE_SIZE);
  273. var view = new DataView(data);
  274. view.setUint32(80, totalTriangles, true);
  275. var offset = 84;
  276. for (var i=0; i<triangleLists.length; i++) {
  277. var triangles = triangleLists[i];
  278. for (var j=0; j<triangles.length; j++) {
  279. setVector(view, offset, triangles[j][3]); // normal
  280. setVector(view, offset+12, triangles[j][0]); // v1
  281. setVector(view, offset+12*2, triangles[j][1]); // v2
  282. setVector(view, offset+12*3, triangles[j][2]); // v3
  283. offset += TRIANGLE_SIZE;
  284. }
  285. }
  286. return view.buffer;
  287. }
  288. function downloadMesh(list) {
  289. if (list.length == 0)
  290. return;
  291. var name = baseFilename;
  292. var toMake = [];
  293. for (var i=0; i<list.length; i++) {
  294. name += "-" + (list[i]+1);
  295. toMake.push(allSplitMeshes[list[i]]);
  296. }
  297. downloadBlob(name+".stl", new Blob([makeMeshByteArray(toMake)], {type: "application/octet-stream"}));
  298. }
  299. function describeBounds(bounds) {
  300. return "("+bounds[0].toFixed(2)+", "+bounds[1].toFixed(2)+", "+bounds[2].toFixed(2)+") - ("+
  301. bounds[3].toFixed(2)+", "+bounds[4].toFixed(2)+", "+bounds[5].toFixed(2)+")";
  302. }
  303. function processSTL(data) {
  304. length = data.byteLength;
  305. view = new DataView(data);
  306. var header = getString(view, 0, 5);
  307. var binary = true;
  308. var text;
  309. if (header == "solid") {
  310. // probably ASCII
  311. text = getString(view, 0, length);
  312. if (text.includes("endfacet")) {
  313. binary = false;
  314. }
  315. }
  316. message(binary ? "binary STL" : "ASCII STL");
  317. triangles = binary ? getBinarySTL(view) : getASCIISTL(text);
  318. message("data successfully read");
  319. splitMesh(triangles);
  320. }
  321. function handleFileSelect(evt) {
  322. var e = document.getElementById('progress').innerHTML = '';
  323. var e = document.getElementById('file');
  324. e.disabled = true;
  325. allSplitMeshes = [];
  326. meshes = [];
  327. document.getElementById('console').innerHTML = '';
  328. var f = evt.target.files[0];
  329. message( "reading "+ String(f.size) + ' bytes');
  330. var n = f.name.split(/[/\\]+/);
  331. baseFilename = f.name.replace(/.*[/\\]/, "").replace(/\.[sS][tT][lL]$/, "");
  332. var reader = new FileReader();
  333. reader.onload = function(event) {
  334. try {
  335. processSTL(event.target.result);
  336. }
  337. catch(err) {
  338. message( "Error: "+err);
  339. var e = document.getElementById('file');
  340. e.disabled = false;
  341. }
  342. }
  343. reader.readAsArrayBuffer(f);
  344. }
  345. document.getElementById('file').addEventListener('change', handleFileSelect, false);
  346. </script>
  347. </body>
  348. </html>