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 final class STL {
22     public float[] normals;
23     public float[] vertices;
24 
25     // this makes only sense  for binary stl format
26     public ubyte[] header = cast(ubyte[])"##Binary STL####################################################################";
27     
28     // in the standard format, attributes should be zero because most software does not understand anything else
29     // public ushort[] attributes; // maybe implement this.
30 
31     package string fname;
32 
33     this(){
34         
35     }
36 
37     @property
38     int numOfTriangles(){
39         if(!vertices.length)
40             return 0;
41         return cast(int)vertices.length/9;
42     }
43 
44     bool empty(){
45         return vertices is null || !vertices.length;
46     }
47 }
48 
49 STL readSTL(string filePath){
50     import std.typecons: tuple;
51     import std.path: baseName;
52     
53     auto stl = new STL();
54 
55     stl.fname = baseName(filePath, ".stl");
56 
57     if(!isBinarySTL(filePath))
58         stl.readAsciiFile(filePath);
59     else
60         stl.readBinaryFile(filePath);
61 
62     return stl;
63 }
64 
65 bool isBinarySTL(S)(auto ref S filePath){
66 
67     auto file = File(filePath, "r");
68     scope(exit) file.close();
69 
70     char[] _line;
71     file.readln(_line);
72     
73     enforce(_line.length, "File is not valid!");
74 
75     if(_line[0..5] == "solid")
76         return false;
77     
78     return true;
79 }
80 
81 void readAsciiFile(S)(STL stl, auto ref S filePath){
82     auto file = File(filePath, "r");
83     scope(exit) file.close();
84 
85     float parseFloat(S)(auto ref S str){
86         return str.to!float;
87     }
88 
89     stl.normals.reserve(20);
90     stl.vertices.reserve(20*3);
91 
92     while (!file.eof){
93         char[] _line;
94         file.readln(_line);
95         _line = chomp(_line);
96         if(!_line.length) continue;
97 
98         string line = assumeUnique(_line);
99 
100         string[] tokens = line.strip.split!isWhite;
101 
102         if(tokens[0] == "vertex"){
103             stl.vertices ~= parseFloat(tokens[1]);
104             stl.vertices ~= parseFloat(tokens[2]);
105             stl.vertices ~= parseFloat(tokens[3]);
106         } else if(tokens[0] == "facet"){
107             stl.normals ~= parseFloat(tokens[2]);
108             stl.normals ~= parseFloat(tokens[3]);
109             stl.normals ~= parseFloat(tokens[4]);
110         }
111             
112     }
113 }
114 
115 void toBinarySTLFile(STL stl, string filePath){
116     File fwriter;
117 
118     fwriter.open(filePath, "wb");
119     scope(exit) fwriter.close();
120 
121     int numOfTri = stl.numOfTriangles;
122 
123     ubyte[] sysBuf = new ubyte[80+4+(12+12+12+12+2) * numOfTri];
124 
125     sysBuf[0..80] = stl.header;
126     
127     sysBuf[80..84] = (cast(ubyte*)&numOfTri)[0..int.sizeof];
128     
129     short _attr = 0;
130 
131     int i;
132     foreach (vchunk, nchunk; zip(chunks(stl.vertices, 9), chunks(stl.normals, 3))){
133 
134         ubyte[50] tbuff;
135 
136         tbuff[0..12] = cast(ubyte[])(nchunk[]);
137         tbuff[12..48] = cast(ubyte[])(vchunk[]);
138         
139         tbuff[48..50] = (cast(ubyte*)&_attr)[0..short.sizeof];
140         
141         sysBuf[84 + i*50 .. 84 + (i+1)*50] = tbuff[];
142         ++i;
143     }
144     
145     fwriter.rawWrite(sysBuf[]);
146 }
147 
148 void readBinaryFile(S)(STL stl, auto ref S filePath){
149     File file;
150     file.open(filePath, "rb");
151     scope(exit) file.close();
152 
153     ubyte[80] _header;
154     file.rawRead(_header[]);
155     stl.header = _header[];
156 
157     ubyte[4] _numOfTriangles;
158     file.rawRead(_numOfTriangles[]);
159 
160     int numOfTriangles = *(cast(int*)_numOfTriangles[].ptr);
161 
162     stl.normals = new float[numOfTriangles * 3];
163     stl.vertices = new float[numOfTriangles * 9];
164 
165     //debug writeln(cast(string)assumeUnique( stl.header));
166     //debug writeln(numOfTriangles);
167     
168     foreach (i; 0..numOfTriangles){
169 
170         ubyte[50] tbuff;
171 
172         file.rawRead(tbuff[]);
173 
174         stl.normals[i*3..(i+1)*3] = cast(float[])tbuff[0..12];
175         stl.vertices[i*9..(i+1)*9] = cast(float[])tbuff[12..48];
176         
177         const short dummy_attr = *(cast(short*)tbuff[48..50].ptr);
178     }
179     
180 }
181 
182 void toAsciiSTLFile(STL stl, string filePath){
183     assert(0, "toAsciiSTLFile is not implemented yet!");
184 }