|
4 | 4 | // (See accompanying file LICENSE_1_0.txt or copy at |
5 | 5 | // https://www.boost.org/LICENSE_1_0.txt) |
6 | 6 |
|
| 7 | +#include "dsga.hxx" |
7 | 8 | #include "stl.hxx" |
| 9 | + |
| 10 | +#include <string> |
8 | 11 | #include <filesystem> |
| 12 | +#include <iostream> |
| 13 | +#include <fstream> |
| 14 | +#include <bit> |
| 15 | + |
9 | 16 | namespace fs = std::filesystem; |
10 | 17 |
|
| 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 | + |
11 | 214 | int stl_main([[maybe_unused]] int argc, [[maybe_unused]] char *argv[]) |
12 | 215 | { |
| 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 | + |
13 | 329 | return EXIT_SUCCESS; |
14 | 330 | } |
0 commit comments