Skip to content

Commit 3843be6

Browse files
committed
added example that converts binary STL files to ASCII STL files
1 parent b616a93 commit 3843be6

File tree

4 files changed

+325
-40
lines changed

4 files changed

+325
-40
lines changed

README.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,6 @@ Current version: `v0.11.0`
246246
247247
### The next steps
248248
* Working on much better API documentation.
249-
* Working on extended example of STL file conversion.
250249
* Working on better ```cmake``` support.
251250
252251
Once we have detailed API documentation and better ```cmake``` support, we can think about releasing a v1.0 version.
@@ -269,7 +268,7 @@ The tests have been most recently run on:
269268
270269
### Windows 11 Native
271270
272-
* **MSVC 2022 - v17.5.5**
271+
* **MSVC 2022 - v17.6.2**
273272
274273
```
275274
[doctest] doctest version is "2.4.11"
@@ -292,7 +291,7 @@ The tests have been most recently run on:
292291
[doctest] Status: SUCCESS!
293292
```
294293
295-
* **clang 16.0.3** on Windows, [official binaries](https://github.com/llvm/llvm-project/releases/tag/llvmorg-16.0.3), with MSVC and/or gcc v13.1.0 installed:
294+
* **clang 16.0.4** on Windows, [official binaries](https://github.com/llvm/llvm-project/releases/tag/llvmorg-16.0.4), with MSVC and/or gcc v13.1.0 installed:
296295
297296
Performs all the unit tests except where there is lack of support for ```std::is_corresponding_member<>```, and this is protected with a feature test macro.
298297
@@ -344,7 +343,7 @@ Performs all the unit tests except where there is lack of support for ```std::is
344343
[doctest] Status: SUCCESS!
345344
```
346345
347-
* **clang 16.0.4**
346+
* **clang 16.0.5**
348347
349348
Performs all the unit tests except where there is lack of support for ```std::is_corresponding_member<>```, and this is protected with a feature test macro.
350349

VS2022/dsga.vcxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
102102
<MultiProcessorCompilation>true</MultiProcessorCompilation>
103103
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
104+
<ControlFlowGuard>Guard</ControlFlowGuard>
104105
</ClCompile>
105106
<Link>
106107
<SubSystem>Console</SubSystem>
@@ -118,6 +119,7 @@
118119
<LanguageStandard>stdcpp20</LanguageStandard>
119120
<MultiProcessorCompilation>true</MultiProcessorCompilation>
120121
<AdditionalOptions>/Ob3 %(AdditionalOptions)</AdditionalOptions>
122+
<ControlFlowGuard>Guard</ControlFlowGuard>
121123
</ClCompile>
122124
<Link>
123125
<SubSystem>Console</SubSystem>
@@ -137,6 +139,7 @@
137139
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
138140
<MultiProcessorCompilation>true</MultiProcessorCompilation>
139141
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
142+
<ControlFlowGuard>Guard</ControlFlowGuard>
140143
</ClCompile>
141144
<Link>
142145
<SubSystem>Console</SubSystem>
@@ -155,6 +158,7 @@
155158
<LanguageStandard>stdcpp20</LanguageStandard>
156159
<MultiProcessorCompilation>true</MultiProcessorCompilation>
157160
<AdditionalOptions>/Ob3 %(AdditionalOptions)</AdditionalOptions>
161+
<ControlFlowGuard>Guard</ControlFlowGuard>
158162
</ClCompile>
159163
<Link>
160164
<SubSystem>Console</SubSystem>

examples/stl.cxx

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,327 @@
44
// (See accompanying file LICENSE_1_0.txt or copy at
55
// https://www.boost.org/LICENSE_1_0.txt)
66

7+
#include "dsga.hxx"
78
#include "stl.hxx"
9+
10+
#include <string>
811
#include <filesystem>
12+
#include <iostream>
13+
#include <fstream>
14+
#include <bit>
15+
916
namespace fs = std::filesystem;
1017

18+
//
19+
// Example: convert a binary STL file to an ASCII STL file, recomputing the normal vectors.
20+
// For this example, and in general, binary STL is assumed to be little-endian.
21+
//
22+
23+
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
24+
25+
// make sure data has no infinities or NaNs
26+
constexpr bool definite_coordinate_triple(const dsga::vec3 &data) noexcept
27+
{
28+
return !(dsga::any(dsga::isinf(data)) || dsga::any(dsga::isnan(data)));
29+
}
30+
31+
// make sure normal vector has no infinities or NaNs and is not the zero-vector { 0, 0, 0 }
32+
constexpr bool valid_normal_vector(const dsga::vec3 &normal) noexcept
33+
{
34+
return definite_coordinate_triple(normal) && dsga::any(dsga::notEqual(normal, dsga::vec3(0)));
35+
}
36+
37+
// not checking for positive-only first octant data -- we are allowing zeros and negative values
38+
constexpr bool valid_vertex_relaxed(const dsga::vec3 &vertex) noexcept
39+
{
40+
return definite_coordinate_triple(vertex);
41+
}
42+
43+
// strict version where all vertex coordinates must be positive-definite
44+
constexpr bool valid_vertex_strict(const dsga::vec3 &vertex) noexcept
45+
{
46+
return definite_coordinate_triple(vertex) && dsga::all(dsga::greaterThan(vertex, dsga::vec3(0)));
47+
}
48+
49+
// right-handed unit normal vector for a triangle facet,
50+
// inputs are triangle vertices in counter-clockwise order
51+
constexpr dsga::vec3 right_handed_normal(const dsga::vec3 &v1, const dsga::vec3 &v2, const dsga::vec3 &v3) noexcept
52+
{
53+
return dsga::normalize(dsga::cross(v2 - v1, v3 - v1));
54+
}
55+
56+
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
57+
58+
// iostream output operator for STL ASCII output -- need to set scientific and precision==9 on the stream
59+
template <dsga::dimensional_scalar T, std::size_t Size>
60+
inline std::ostream &operator<<(std::ostream &o, const dsga::basic_vector<T, Size> &v)
61+
{
62+
o << v[0];
63+
for (int i = 1; i < v.length(); ++i)
64+
o << " " << v[i];
65+
return o;
66+
}
67+
68+
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
69+
70+
bool maybe_binary_stl(std::ifstream &some_file, uintmax_t size)
71+
{
72+
constexpr auto facet_size = 50u;
73+
constexpr auto header_size = 80u;
74+
constexpr auto num_facets_size = 4u;
75+
bool maybe_val = false;
76+
unsigned int num_facets{};
77+
78+
// file too small to have bytes for number of facets
79+
if (size < (header_size + num_facets_size))
80+
return false;
81+
82+
// skip possible header and read possible number of facets
83+
some_file.seekg(header_size);
84+
some_file.read(reinterpret_cast<char *>(&num_facets), num_facets_size);
85+
86+
if constexpr (std::endian::native == std::endian::big)
87+
num_facets = dsga::byteswap(num_facets);
88+
89+
uintmax_t hypothetical_size = ((num_facets * facet_size) + header_size + num_facets_size);
90+
maybe_val = (hypothetical_size == size);
91+
92+
return maybe_val;
93+
}
94+
95+
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
96+
97+
constexpr auto solid_open = "solid dsga_example\n";
98+
constexpr auto facet_open = " facet normal ";
99+
constexpr auto loop_open = " outer loop\n";
100+
constexpr auto vertex_line = " vertex ";
101+
constexpr auto loop_close = " endloop\n";
102+
constexpr auto facet_close = " endfacet\n";
103+
constexpr auto solid_close = "endsolid dsga_example\n";
104+
105+
void write_ascii_facet(std::ofstream &out_file, const dsga::vec3 &computed_normal, const dsga::vec3 &vertex1, const dsga::vec3 &vertex2, const dsga::vec3 &vertex3)
106+
{
107+
out_file << facet_open << computed_normal << "\n";
108+
out_file << loop_open;
109+
out_file << vertex_line << vertex1 << "\n";
110+
out_file << vertex_line << vertex2 << "\n";
111+
out_file << vertex_line << vertex3 << "\n";
112+
out_file << loop_close;
113+
out_file << facet_close;
114+
}
115+
116+
bool read_binary_stl_float(std::ifstream &some_file, float &float_val)
117+
{
118+
constexpr auto float_size = 4u;
119+
some_file.read(reinterpret_cast<char *>(&float_val), float_size);
120+
121+
// make sure we read the number of bytes we wanted
122+
if (some_file.gcount() != float_size)
123+
return false;
124+
125+
if constexpr (std::endian::native == std::endian::big)
126+
float_val = dsga::byteswap(float_val);
127+
128+
return true;
129+
}
130+
131+
bool read_coordinate_triple(std::ifstream &some_file, dsga::vec3 &triple)
132+
{
133+
return
134+
read_binary_stl_float(some_file, triple[0]) &&
135+
read_binary_stl_float(some_file, triple[1]) &&
136+
read_binary_stl_float(some_file, triple[2]);
137+
}
138+
139+
bool read_facet_vertices(std::ifstream &some_file, dsga::vec3 &vertex1, dsga::vec3 &vertex2, dsga::vec3 &vertex3)
140+
{
141+
return
142+
read_coordinate_triple(some_file, vertex1) &&
143+
read_coordinate_triple(some_file, vertex2) &&
144+
read_coordinate_triple(some_file, vertex3);
145+
}
146+
147+
bool read_binary_facet_write_ascii_facet(std::ifstream &some_file, std::ofstream &out_file)
148+
{
149+
const std::streampos facet_size = 50;
150+
constexpr auto normal_size = 12u;
151+
152+
// remember where we started reading
153+
auto file_cursor = some_file.tellg();
154+
155+
bool success = false;
156+
157+
// skip over normal vector in file -- we don't trust it -- we recompute it from vertices
158+
some_file.ignore(normal_size);
159+
160+
dsga::vec3 vertex1;
161+
dsga::vec3 vertex2;
162+
dsga::vec3 vertex3;
163+
bool valid_vertices = read_facet_vertices(some_file, vertex1, vertex2, vertex3);
164+
165+
// check for infinities and NaNs -- don't worry about non-zero first octant vertex location
166+
if (valid_vertices && valid_vertex_relaxed(vertex1) && valid_vertex_relaxed(vertex2) && valid_vertex_relaxed(vertex3))
167+
{
168+
auto computed_normal = right_handed_normal(vertex1, vertex2, vertex3);
169+
if (valid_normal_vector(computed_normal))
170+
{
171+
write_ascii_facet(out_file, computed_normal, vertex1, vertex2, vertex3);
172+
success = true;
173+
}
174+
else
175+
{
176+
success = false; // don't need this here, but it is explicit for reading purposes
177+
}
178+
}
179+
180+
// set position to next binary facet
181+
some_file.seekg(file_cursor + facet_size);
182+
return success;
183+
}
184+
185+
bool convert_binary_stl_to_ascii(std::ifstream &some_file, std::ofstream &out_file, unsigned num_facets)
186+
{
187+
constexpr auto header_size = 80u;
188+
constexpr auto num_facets_size = 4u;
189+
190+
some_file.seekg(header_size + num_facets_size);
191+
192+
// iostream ASCII STL float format
193+
out_file.setf(std::ios::scientific);
194+
out_file.precision(9);
195+
196+
// convert input to output
197+
out_file << solid_open;
198+
unsigned bad_facets = 0;
199+
for (unsigned i = 0; i < num_facets; ++i)
200+
{
201+
if (!read_binary_facet_write_ascii_facet(some_file, out_file))
202+
++bad_facets;
203+
}
204+
out_file << solid_close;
205+
206+
if (bad_facets)
207+
std::cerr << "number of bad facets: " << bad_facets << std::endl;
208+
209+
return bad_facets != num_facets;
210+
}
211+
212+
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
213+
11214
int stl_main([[maybe_unused]] int argc, [[maybe_unused]] char *argv[])
12215
{
216+
if (argc != 3)
217+
{
218+
std::cout << "Convert binary STL file to ASCII STL file.\n";
219+
std::cout << "Usage: " << argv[0] << " binary_src.stl ascii_dest.stl\n";
220+
return EXIT_FAILURE;
221+
}
222+
else
223+
{
224+
// check if input file appears to be a binary STL file
225+
auto binary_stl_path = fs::path(argv[1]);
226+
227+
auto binary_exists = fs::exists(binary_stl_path);
228+
auto binary_regular = binary_exists ? fs::is_regular_file(binary_stl_path) : false;
229+
auto binary_size = binary_exists ? fs::file_size(binary_stl_path) : 0;
230+
231+
// do file size and facet count make sense for this to be a binary STL file?
232+
bool appears_to_be_binary_stl = false;
233+
if (binary_exists && binary_regular)
234+
{
235+
auto maybe_binary_stl_file = std::ifstream(binary_stl_path, std::ios::binary);
236+
if (maybe_binary_stl_file.is_open())
237+
{
238+
appears_to_be_binary_stl = maybe_binary_stl(maybe_binary_stl_file, binary_size);
239+
maybe_binary_stl_file.close();
240+
}
241+
}
242+
243+
// early exit if bad input file
244+
if (!appears_to_be_binary_stl)
245+
{
246+
std::cerr << fs::absolute(binary_stl_path) << " is not a valid binary STL file.\n";
247+
return EXIT_FAILURE;
248+
}
249+
250+
// check if output file can be written as an ASCII STL file
251+
auto ascii_stl_path = fs::path(argv[2]);
252+
253+
// can't have same source and destination
254+
if (fs::absolute(binary_stl_path) == fs::absolute(ascii_stl_path))
255+
{
256+
std::cerr << "Input file must be different from output file.\n";
257+
return EXIT_FAILURE;
258+
}
259+
260+
auto ascii_exists = fs::exists(ascii_stl_path);
261+
auto ascii_regular = ascii_exists ? fs::is_regular_file(ascii_stl_path) : false;
262+
263+
[[maybe_unused]] bool overwrite_destination = false;
264+
[[maybe_unused]] bool new_destination = false;
265+
if (ascii_exists)
266+
{
267+
if (ascii_regular)
268+
{
269+
std::string user_input{};
270+
std::cout << "Overwrite " << fs::absolute(ascii_stl_path) << "? [Y/n] ";
271+
std::getline(std::cin, user_input);
272+
if (user_input.empty() || user_input[0] == 'y' || user_input[0] == 'Y')
273+
{
274+
std::ofstream ascii_file(ascii_stl_path);
275+
if (ascii_file.is_open())
276+
{
277+
overwrite_destination = true;
278+
ascii_file.close();
279+
}
280+
}
281+
else
282+
{
283+
std::cerr << fs::absolute(ascii_stl_path) << " will not be overwritten.\n";
284+
}
285+
}
286+
else
287+
{
288+
std::cerr << fs::absolute(ascii_stl_path) << " is a bad path for destination.\n";
289+
}
290+
}
291+
else
292+
{
293+
std::ofstream ascii_file(ascii_stl_path);
294+
if (ascii_file.is_open())
295+
{
296+
new_destination = true;
297+
ascii_file.close();
298+
}
299+
}
300+
301+
// ascii destination file verified ok
302+
if (overwrite_destination || new_destination)
303+
{
304+
constexpr auto facet_size = 50u;
305+
constexpr auto header_size = 80u;
306+
constexpr auto num_facets_size = 4u;
307+
308+
auto num_facets = (binary_size - header_size - num_facets_size) / facet_size;
309+
310+
auto binary_stl = std::ifstream(binary_stl_path, std::ios::binary);
311+
auto ascii_stl = std::ofstream(ascii_stl_path);
312+
bool success = convert_binary_stl_to_ascii(binary_stl, ascii_stl, static_cast<unsigned>(num_facets));
313+
binary_stl.close();
314+
ascii_stl.close();
315+
316+
if (!success)
317+
{
318+
std::cerr << "No good facets found.\n";
319+
return EXIT_FAILURE;
320+
}
321+
}
322+
else
323+
{
324+
std::cerr << "Can't open destination file " << fs::absolute(ascii_stl_path) << "\n";
325+
return EXIT_FAILURE;
326+
}
327+
}
328+
13329
return EXIT_SUCCESS;
14330
}

0 commit comments

Comments
 (0)