1 /** Some utility for reading and writing both ASCII and binary STL files 2 3 based on https://en.wikipedia.org/wiki/STL_(file_format) 4 5 Copyright: 6 Copyright (c) 2021, Ferhat Kurtulmuş. 7 8 License: 9 $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0). 10 */ 11 module stlutils; 12 13 import std.stdio; 14 import std.exception; 15 import std..string; 16 import std.uni : isWhite; 17 import std.array : split; 18 import std.range; 19 import std.conv; 20 21 // TODO: use exceptions more for error handling 22 23 final class STL { 24 public float[] normals; 25 public float[] vertices; 26 27 // this makes only sense for binary stl format 28 public ubyte[80] header = cast(ubyte[80])"##Binary STL####################################################################"; 29 30 // in the standard format, attributes should be zero because most software does not understand anything else 31 // public ushort[] attributes; // maybe implement this. 32 33 package string fname; 34 35 this(){ 36 37 } 38 39 @property 40 int numOfTriangles(){ 41 if(!vertices.length) 42 return 0; 43 return cast(int)vertices.length/9; 44 } 45 46 bool empty(){ 47 return vertices is null || !vertices.length; 48 } 49 } 50 51 STL readSTL(string filePath){ 52 import std.path: baseName; 53 54 auto stl = new STL(); 55 56 stl.fname = baseName(filePath, ".stl"); 57 58 if(!isBinarySTL(filePath)) 59 stl.readAsciiFile(filePath); 60 else 61 stl.readBinaryFile(filePath); 62 63 return stl; 64 } 65 66 bool isBinarySTL(S)(auto ref S filePath){ 67 68 auto file = File(filePath, "r"); 69 scope(exit) file.close(); 70 71 char[] _line; 72 file.readln(_line); 73 74 enforce(_line.length, "File is not valid!"); 75 76 if(_line[0..5] == "solid") 77 return false; 78 79 return true; 80 } 81 82 void readAsciiFile(S)(STL stl, auto ref S filePath){ 83 auto file = File(filePath, "r"); 84 scope(exit) file.close(); 85 86 float parseFloat(S)(auto ref S str){ 87 return str.to!float; 88 } 89 90 import std.algorithm.searching : startsWith; 91 ulong nVertexLine; 92 while (!file.eof){ 93 char[] _line; 94 file.readln(_line); 95 if(_line.chomp.strip.startsWith("vertex")) 96 ++nVertexLine; 97 } 98 99 file.seek(0); 100 101 stl.vertices = new float[nVertexLine*3]; 102 stl.normals = new float[nVertexLine]; 103 104 ulong vcur, ncur; 105 106 while (!file.eof){ 107 char[] _line; 108 file.readln(_line); 109 auto line = chomp(_line); 110 if(!line.length) continue; 111 112 auto tokens = line.strip.split!isWhite; 113 114 if(tokens[0] == "vertex"){ 115 stl.vertices[vcur++] = parseFloat(tokens[1]); 116 stl.vertices[vcur++] = parseFloat(tokens[2]); 117 stl.vertices[vcur++] = parseFloat(tokens[3]); 118 } else if(tokens[0] == "facet"){ 119 stl.normals[ncur++] = parseFloat(tokens[2]); 120 stl.normals[ncur++] = parseFloat(tokens[3]); 121 stl.normals[ncur++] = parseFloat(tokens[4]); 122 } 123 } 124 } 125 126 /* // write-at-once 127 void toBinarySTLFile(STL stl, string filePath){ 128 File fwriter; 129 130 fwriter.open(filePath, "wb"); 131 scope(exit) fwriter.close(); 132 133 int numOfTri = stl.numOfTriangles; 134 135 ubyte[] sysBuf = new ubyte[80+4+(12+12+12+12+2) * numOfTri]; 136 137 sysBuf[0..80] = stl.header[]; 138 139 sysBuf[80..84] = (cast(ubyte*)&numOfTri)[0..int.sizeof]; 140 141 short _attr = 0; 142 143 int i; 144 foreach (vchunk, nchunk; zip(chunks(stl.vertices, 9), chunks(stl.normals, 3))){ 145 146 ubyte[50] tbuff; 147 148 tbuff[0..12] = cast(ubyte[])(nchunk[]); 149 tbuff[12..48] = cast(ubyte[])(vchunk[]); 150 151 tbuff[48..50] = (cast(ubyte*)&_attr)[0..short.sizeof]; 152 153 sysBuf[84 + i*50 .. 84 + (i+1)*50] = tbuff[]; 154 ++i; 155 } 156 157 fwriter.rawWrite(sysBuf[]); 158 } 159 */ 160 161 void toBinarySTLFile(STL stl, string filePath){ 162 File fwriter; 163 164 fwriter.open(filePath, "wb"); 165 scope(exit) fwriter.close(); 166 167 int numOfTri = stl.numOfTriangles; 168 169 fwriter.rawWrite(stl.header[]); 170 171 fwriter.rawWrite((cast(ubyte*)&numOfTri)[0..int.sizeof]); 172 173 short _attr = 0; 174 175 foreach (vchunk, nchunk; zip(chunks(stl.vertices, 9), chunks(stl.normals, 3))){ 176 177 ubyte[50] tbuff; 178 179 tbuff[0..12] = cast(ubyte[])(nchunk[]); 180 tbuff[12..48] = cast(ubyte[])(vchunk[]); 181 182 tbuff[48..50] = (cast(ubyte*)&_attr)[0..short.sizeof]; 183 184 fwriter.rawWrite(tbuff[]); 185 } 186 } 187 188 void readBinaryFile(S)(STL stl, auto ref S filePath){ 189 File file; 190 file.open(filePath, "rb"); 191 scope(exit) file.close(); 192 193 ubyte[80] _header; 194 file.rawRead(_header[]); 195 stl.header[] = _header[]; 196 197 ubyte[4] _numOfTriangles; 198 file.rawRead(_numOfTriangles[]); 199 200 int numOfTriangles = *(cast(int*)_numOfTriangles[].ptr); 201 202 stl.normals = new float[numOfTriangles * 3]; 203 stl.vertices = new float[numOfTriangles * 9]; 204 205 //debug writeln(cast(string)assumeUnique( stl.header)); 206 //debug writeln(numOfTriangles); 207 208 foreach (i; 0..numOfTriangles){ 209 210 ubyte[50] tbuff; 211 212 file.rawRead(tbuff[]); 213 214 stl.normals[i*3..(i+1)*3] = cast(float[])tbuff[0..12]; 215 stl.vertices[i*9..(i+1)*9] = cast(float[])tbuff[12..48]; 216 217 const short dummy_attr = *(cast(short*)tbuff[48..50].ptr); 218 } 219 220 } 221 222 void toAsciiSTLFile(STL stl, string filePath){ 223 File file; 224 file.open(filePath, "w"); 225 scope(exit) file.close(); 226 227 file.writeln("solid STLExport"); 228 229 foreach (vchunk, nchunk; zip(chunks(stl.vertices, 9), chunks(stl.normals, 3))){ 230 file.writefln("facet normal %.6g %.6g %.6g", nchunk[0], nchunk[1], nchunk[2]); 231 file.writeln(" outer loop"); 232 file.writefln(" vertex %.6g %.6g %.6g", vchunk[0], vchunk[1], vchunk[2]); 233 file.writefln(" vertex %.6g %.6g %.6g", vchunk[3], vchunk[4], vchunk[5]); 234 file.writefln(" vertex %.6g %.6g %.6g", vchunk[6], vchunk[7], vchunk[8]); 235 file.writeln(" endloop"); 236 file.writeln("endfacet"); 237 } 238 239 file.writeln("endsolid STLExport"); 240 241 } 242 243 void toOBJFile(STL stl, string filePath){ 244 File file; 245 file.open(filePath, "w"); 246 scope(exit) file.close(); 247 248 file.writeln("#"); 249 file.writeln("# object STLExport"); 250 file.writeln("#"); 251 file.write("\n"); 252 253 foreach (v; chunks(stl.vertices, 9)){ 254 file.writefln("v %.6g %.6g %.6g", v[0], v[1], v[2]); 255 file.writefln("v %.6g %.6g %.6g", v[3], v[4], v[5]); 256 file.writefln("v %.6g %.6g %.6g", v[6], v[7], v[8]); 257 } 258 259 file.writefln("# %u vertices", stl.vertices.length/9); 260 261 file.write("\n"); 262 263 foreach (n; chunks(stl.normals, 3)){ 264 file.writefln("vn %.6g %.6g %.6g", n[0], n[1], n[2]); 265 } 266 267 file.writefln("# %u vertex normals", stl.normals.length/3); 268 269 file.write("\n"); 270 271 file.writeln("g STLExport"); 272 file.writeln("s 1"); 273 274 ulong a1 = 1, a2 = 1, b1 = 2, b2 = 1, c1 = 3, c2 = 1; 275 276 foreach ( _ ; 0..stl.vertices.length/9){ 277 file.writefln("f %u//%u %u//%u %u//%u", a1, a2, b1, b2, c1, c2); 278 a1 += 3; 279 a2 += 1; 280 b1 += 3; 281 b2 += 1; 282 c1 += 3; 283 c2 += 1; 284 } 285 286 file.writefln("# %u polygons", stl.vertices.length/9); 287 }