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 }