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     // maybe preallocate vertices and normals based on the number of the lines
91     stl.normals.reserve(20);
92     stl.vertices.reserve(20*9);
93 
94     while (!file.eof){
95         char[] _line;
96         file.readln(_line);
97         _line = chomp(_line);
98         if(!_line.length) continue;
99 
100         string line = assumeUnique(_line);
101 
102         string[] tokens = line.strip.split!isWhite;
103 
104         if(tokens[0] == "vertex"){
105             stl.vertices ~= parseFloat(tokens[1]);
106             stl.vertices ~= parseFloat(tokens[2]);
107             stl.vertices ~= parseFloat(tokens[3]);
108         } else if(tokens[0] == "facet"){
109             stl.normals ~= parseFloat(tokens[2]);
110             stl.normals ~= parseFloat(tokens[3]);
111             stl.normals ~= parseFloat(tokens[4]);
112         }
113             
114     }
115 }
116 
117 /* // write-at-once
118 void toBinarySTLFile(STL stl, string filePath){
119     File fwriter;
120 
121     fwriter.open(filePath, "wb");
122     scope(exit) fwriter.close();
123 
124     int numOfTri = stl.numOfTriangles;
125 
126     ubyte[] sysBuf = new ubyte[80+4+(12+12+12+12+2) * numOfTri];
127 
128     sysBuf[0..80] = stl.header[];
129     
130     sysBuf[80..84] = (cast(ubyte*)&numOfTri)[0..int.sizeof];
131     
132     short _attr = 0;
133 
134     int i;
135     foreach (vchunk, nchunk; zip(chunks(stl.vertices, 9), chunks(stl.normals, 3))){
136 
137         ubyte[50] tbuff;
138 
139         tbuff[0..12] = cast(ubyte[])(nchunk[]);
140         tbuff[12..48] = cast(ubyte[])(vchunk[]);
141         
142         tbuff[48..50] = (cast(ubyte*)&_attr)[0..short.sizeof];
143         
144         sysBuf[84 + i*50 .. 84 + (i+1)*50] = tbuff[];
145         ++i;
146     }
147     
148     fwriter.rawWrite(sysBuf[]);
149 }
150 */
151 
152 void toBinarySTLFile(STL stl, string filePath){
153     File fwriter;
154 
155     fwriter.open(filePath, "wb");
156     scope(exit) fwriter.close();
157 
158     int numOfTri = stl.numOfTriangles;
159 
160     fwriter.rawWrite(stl.header[]);
161     
162     fwriter.rawWrite((cast(ubyte*)&numOfTri)[0..int.sizeof]);
163     
164     short _attr = 0;
165     
166     foreach (vchunk, nchunk; zip(chunks(stl.vertices, 9), chunks(stl.normals, 3))){
167 
168         ubyte[50] tbuff;
169 
170         tbuff[0..12] = cast(ubyte[])(nchunk[]);
171         tbuff[12..48] = cast(ubyte[])(vchunk[]);
172         
173         tbuff[48..50] = (cast(ubyte*)&_attr)[0..short.sizeof];
174 
175         fwriter.rawWrite(tbuff[]);
176     }
177 }
178 
179 void readBinaryFile(S)(STL stl, auto ref S filePath){
180     File file;
181     file.open(filePath, "rb");
182     scope(exit) file.close();
183 
184     ubyte[80] _header;
185     file.rawRead(_header[]);
186     stl.header[] = _header[];
187 
188     ubyte[4] _numOfTriangles;
189     file.rawRead(_numOfTriangles[]);
190 
191     int numOfTriangles = *(cast(int*)_numOfTriangles[].ptr);
192 
193     stl.normals = new float[numOfTriangles * 3];
194     stl.vertices = new float[numOfTriangles * 9];
195 
196     //debug writeln(cast(string)assumeUnique( stl.header));
197     //debug writeln(numOfTriangles);
198     
199     foreach (i; 0..numOfTriangles){
200 
201         ubyte[50] tbuff;
202 
203         file.rawRead(tbuff[]);
204 
205         stl.normals[i*3..(i+1)*3] = cast(float[])tbuff[0..12];
206         stl.vertices[i*9..(i+1)*9] = cast(float[])tbuff[12..48];
207         
208         const short dummy_attr = *(cast(short*)tbuff[48..50].ptr);
209     }
210     
211 }
212 
213 void toAsciiSTLFile(STL stl, string filePath){
214     File file;
215     file.open(filePath, "w");
216     scope(exit) file.close();
217 
218     file.writeln("solid STLExport");
219 
220     foreach (vchunk, nchunk; zip(chunks(stl.vertices, 9), chunks(stl.normals, 3))){
221         file.writefln("facet normal %f %f %f", nchunk[0], nchunk[1], nchunk[2]);
222         file.writeln("   outer loop");
223         file.writefln("      vertex %f %f %f", vchunk[0], vchunk[1], vchunk[2]);
224         file.writefln("      vertex %f %f %f", vchunk[3], vchunk[4], vchunk[5]);
225         file.writefln("      vertex %f %f %f", vchunk[6], vchunk[7], vchunk[8]);
226         file.writeln("   endloop");
227         file.writeln("endfacet");
228     }
229 
230     file.writeln("endsolid STLExport");
231 
232 }
233 
234 void toOBJFile(STL stl, string filePath){
235     File file;
236     file.open(filePath, "w");
237     scope(exit) file.close();
238 
239     file.writeln("#");
240     file.writeln("# object STLExport");
241     file.writeln("#");
242     file.write("\n");
243 
244     foreach (v; chunks(stl.vertices, 9)){
245         file.writefln("v %f %f %f", v[0], v[1], v[2]);
246         file.writefln("v %f %f %f", v[3], v[4], v[5]);
247         file.writefln("v %f %f %f", v[6], v[7], v[8]);
248     }
249 
250     file.writefln("# %u vertices", stl.vertices.length/9);
251 
252     file.write("\n");
253 
254     foreach (n; chunks(stl.normals, 3)){
255         file.writefln("vn %f %f %f", n[0], n[1], n[2]);
256     }
257 
258     file.writefln("# %u vertex normals", stl.normals.length/3);
259 
260     file.write("\n");
261     
262     file.writeln("g STLExport");
263     file.writeln("s 1");
264 
265     ulong a1 = 1, a2 = 1, b1 = 2, b2 = 1, c1 = 3, c2 = 1;
266 
267     foreach ( _ ; 0..stl.vertices.length/9){
268         file.writefln("f %u//%u %u//%u %u//%u", a1, a2, b1, b2, c1, c2);
269         a1 += 3;
270         a2 += 1;
271         b1 += 3;
272         b2 += 1;
273         c1 += 3;
274         c2 += 1;
275     }
276 
277     file.writefln("# %u polygons", stl.vertices.length/9);
278 }