Something went wrong. Try again.
atproto Thingiverse but good
Something went wrong. Try again.
16 kB · 422 lines
C++
at commit bb68bf8c
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423#include <LDParse/Color.hpp>#include <LDParse/Lex.hpp>#include <LDParse/ModelBuilder/ModelBuilderDefs.hpp>#include <LDParse/ModelBuilder/ModelBuilderImpl.hpp>
#include <algorithm>#include <cctype>#include <cstdlib>#include <filesystem>#include <fstream>#include <iomanip>#include <iostream>#include <optional>#include <set>#include <sstream>#include <string>#include <unordered_set>#include <vector>
namespace {
struct Diagnostic { std::string message; std::string token; bool fatal;};
thread_local std::vector<Diagnostic>* g_diagnostics = nullptr;
std::string json_escape(const std::string& value) { std::ostringstream out; for (unsigned char ch : value) { switch (ch) { case '"': out << "\\\""; break; case '\\': out << "\\\\"; break; case '\n': out << "\\n"; break; case '\r': out << "\\r"; break; case '\t': out << "\\t"; break; default: if (ch < 0x20) { out << "\\u" << std::hex << std::setw(4) << std::setfill('0') << static_cast<int>(ch) << std::dec; } else { out << static_cast<char>(ch); } } } return out.str();}
std::string quote(const std::string& value) { return "\"" + json_escape(value) + "\""; }
struct ResolvedCounts { size_t triangles = 0; size_t quads = 0; size_t lines = 0; size_t conditional_lines = 0; size_t includes = 0;};
struct Counts { size_t triangles = 0; size_t quads = 0; size_t lines = 0; size_t conditional_lines = 0; size_t includes = 0;};
void add_counts(ResolvedCounts& destination, const Counts& source) { destination.triangles += source.triangles; destination.quads += source.quads; destination.lines += source.lines; destination.conditional_lines += source.conditional_lines; destination.includes += source.includes;}
std::string lowercase(std::string value) { std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) { return static_cast<char>(std::tolower(ch)); }); return value;}
std::optional<std::filesystem::path> resolve_part(const std::string& ldrawDir, const std::string& name) { const std::filesystem::path root(ldrawDir); const std::vector<std::filesystem::path> candidates = { root / "parts" / name, root / "p" / name, root / "parts" / "s" / name, };
for (const auto& candidate : candidates) { std::ifstream input(candidate, std::ios::binary); if (input) return candidate;
const auto parent = candidate.parent_path(); std::error_code error; if (!std::filesystem::is_directory(parent, error)) continue; const auto wanted = lowercase(candidate.filename().string()); for (const auto& entry : std::filesystem::directory_iterator(parent, error)) { if (error) break; if (lowercase(entry.path().filename().string()) == wanted) return entry.path(); } } return std::nullopt;}
std::optional<std::string> include_name(const LDParse::TokenStream& tokens, const std::string& line) { if (tokens.empty() || tokens[0].k != LDParse::One || tokens.size() <= 14) return std::nullopt; const auto filename_start = tokens[14].c; if (filename_start >= line.size()) return std::nullopt; std::string name = line.substr(filename_start); while (!name.empty() && std::isspace(static_cast<unsigned char>(name.back()))) name.pop_back(); return name.empty() ? std::nullopt : std::optional<std::string>(std::move(name));}
Counts count_geometry(const LDParse::ModelStream& models) { Counts counts; for (const auto& [modelName, lineStream] : models) { (void)modelName; for (const auto& [lineText, tokens] : lineStream) { (void)lineText; if (tokens.empty()) continue; switch (tokens[0].k) { case LDParse::One: counts.includes++; break; case LDParse::Two: counts.lines++; break; case LDParse::Three: counts.triangles++; break; case LDParse::Four: counts.quads++; break; case LDParse::Five: counts.conditional_lines++; break; default: break; } } } return counts;}
std::optional<LDParse::ModelStream> tokenize_file(const std::filesystem::path& path, LDParse::ErrF error_handler) { std::ifstream input(path, std::ios::binary); if (!input) return std::nullopt; std::ostringstream contents; contents << input.rdbuf(); std::istringstream stream(contents.str()); LDParse::Lexer<LDParse::ErrF> lexer(stream, error_handler); LDParse::ModelStream models; std::string root = path.filename().string(); lexer.lexModelBoundaries(models, root); return models;}
void resolve_includes(const std::string& ldrawDir, const LDParse::ModelStream& models, const std::set<std::string>& modelNames, std::set<std::string>& visited, ResolvedCounts& counts, LDParse::ErrF error_handler) { for (const auto& [modelName, lineStream] : models) { (void)modelName; for (const auto& [lineText, tokens] : lineStream) { const auto name = include_name(tokens, lineText); if (!name) continue;
const auto key = lowercase(*name); if (modelNames.count(key)) continue; if (!visited.insert(key).second) continue; const auto path = resolve_part(ldrawDir, *name); if (!path) continue; const auto partModels = tokenize_file(*path, error_handler); if (!partModels) continue;
add_counts(counts, count_geometry(*partModels)); resolve_includes(ldrawDir, *partModels, modelNames, visited, counts, error_handler); } }}
std::string certification(boost::logic::tribool value) { if (boost::logic::indeterminate(value)) return "unknown"; return value ? "certified" : "uncertified";}
std::string winding_name(LDParse::BFCStatus w) { switch (w) { case LDParse::BFCOff: return "off"; case LDParse::Standard: return "ccw"; case LDParse::Invert: return "cw"; default: return "unknown"; }}
std::string pos_json(const LDParse::Position& p) { return "[" + std::to_string(std::get<0>(p)) + "," + std::to_string(std::get<1>(p)) + "," + std::to_string(std::get<2>(p)) + "]";}
std::string matrix_json(const LDParse::TransMatrix& m) { const auto& [t, m0, m1, m2, m3, m4, m5, m6, m7, m8] = m; return "{\"translation\":" + pos_json(t) + ",\"matrix\":[" + std::to_string(m0) + "," + std::to_string(m1) + "," + std::to_string(m2) + "," + std::to_string(m3) + "," + std::to_string(m4) + "," + std::to_string(m5) + "," + std::to_string(m6) + "," + std::to_string(m7) + "," + std::to_string(m8) + "]}";}
std::string diagnostics_json(const std::vector<Diagnostic>& diagnostics) { std::ostringstream out; out << '['; for (size_t i = 0; i < diagnostics.size(); ++i) { if (i) out << ','; out << "{\"message\":" << quote(diagnostics[i].message) << ",\"token\":" << quote(diagnostics[i].token) << ",\"fatal\":" << (diagnostics[i].fatal ? "true" : "false") << '}'; } return out.str() + ']';}
std::string mesh_json(const LDParse::LDMesh& mesh) { const auto& positions = std::get<0>(mesh.attributes); const auto& normals = std::get<1>(mesh.attributes);
std::ostringstream out; out << "{\"vertex_count\":" << positions.size();
out << ",\"positions\":["; for (size_t i = 0; i < positions.size(); ++i) { if (i) out << ','; out << pos_json(positions[i]); } out << ']';
out << ",\"triangle_count\":" << (mesh.indices.size() / 3); out << ",\"triangles\":["; for (size_t i = 0; i + 2 < mesh.indices.size(); i += 3) { if (i) out << ','; out << "[" << mesh.indices[i] << "," << mesh.indices[i+1] << "," << mesh.indices[i+2] << "]"; } out << ']';
out << ",\"bf_triangle_count\":" << (mesh.bfIndices.size() / 3);
return out.str() + '}';}
std::string children_json(const decltype(std::declval<LDParse::Model>().children())& children) { std::ostringstream out; out << '['; for (size_t i = 0; i < children.size(); ++i) { if (i) out << ','; const auto& [idx, bfc, transform, childPtr] = children[i]; out << "{\"index\":" << idx << ",\"bfc\":\"" << winding_name(bfc) << "\"" << ",\"transform\":" << matrix_json(transform) << ",\"child_name\":" << (childPtr ? quote(childPtr->name()) : "null") << '}'; } return out.str() + ']';}
std::string model_json(const LDParse::Model& model) { const auto& colors = model.localColors(); std::vector<std::pair<uint16_t, uint16_t>> sortedColors(colors.begin(), colors.end()); std::sort(sortedColors.begin(), sortedColors.end());
std::ostringstream out; out << "{\"name\":" << quote(model.name()) << ",\"certify\":" << quote(certification(model.certify())) << ",\"winding\":\"" << winding_name(model.winding()) << "\"" << ",\"default_color\":" << model.defaultColor() << ",\"step_count\":" << model.stepEnds().size() << ",\"local_colors\":["; for (size_t i = 0; i < sortedColors.size(); ++i) { if (i) out << ','; out << "{\"code\":" << sortedColors[i].first << ",\"slot\":" << sortedColors[i].second << '}'; } out << "]" << ",\"mesh\":" << mesh_json(model.mesh()) << ",\"children\":" << children_json(model.children()) << '}'; return out.str();}
std::string success_json(const std::string& path, const std::vector<std::string>& names, const LDParse::Model& root, const std::vector<Diagnostic>& diagnostics, size_t t_tri, size_t t_quad, size_t t_line, size_t t_cond, size_t t_incl, const ResolvedCounts& resolved, const std::vector<std::string>& tri_positions) { std::ostringstream out; out << "{\"schema_version\":\"ldraw-oracle-v2\"" << ",\"fixture_path\":" << quote(path) << ",\"status\":\"ok\"" << ",\"model_names\":["; for (size_t i = 0; i < names.size(); ++i) { if (i) out << ','; out << quote(names[i]); } out << "]" << ",\"root_model\":" << model_json(root) << ",\"tokenized_geometry\":{" << "\"triangles\":" << t_tri << ",\"quads\":" << t_quad << ",\"lines\":" << t_line << ",\"conditional_lines\":" << t_cond << ",\"includes\":" << t_incl << ",\"triangle_positions\":["; for (size_t i = 0; i < tri_positions.size(); ++i) { if (i) out << ','; out << tri_positions[i]; } out << "]}" << ",\"resolved_geometry\":{\"triangles\":" << resolved.triangles << ",\"quads\":" << resolved.quads << ",\"lines\":" << resolved.lines << ",\"conditional_lines\":" << resolved.conditional_lines << ",\"includes\":" << resolved.includes << "}" << ",\"diagnostics\":" << diagnostics_json(diagnostics) << "}"; return out.str();}
std::string error_json(const std::string& path, const std::string& kind, const std::string& message, const std::vector<Diagnostic>& diagnostics) { std::ostringstream out; out << "{\"schema_version\":\"ldraw-oracle-v2\"" << ",\"fixture_path\":" << quote(path) << ",\"status\":\"error\"" << ",\"diagnostics\":" << diagnostics_json(diagnostics) << ",\"error\":{\"kind\":" << quote(kind) << ",\"message\":" << quote(message) << "}" << "}"; return out.str();}
int run_one(const std::string& path) { std::ifstream input(path, std::ios::binary); if (!input) { std::vector<Diagnostic> diags{{"could not open fixture", path, true}}; std::cout << error_json(path, "open_failed", "could not open fixture", diags) << '\n'; return 1; } std::ostringstream contents; contents << input.rdbuf(); const std::string bytes = contents.str();
std::istringstream boundaries_stream(bytes); std::vector<Diagnostic> diagnostics; g_diagnostics = &diagnostics; LDParse::ErrF boundary_error = +[](std::string message, std::string token, bool fatal) { if (g_diagnostics) g_diagnostics->push_back({std::move(message), std::move(token), fatal}); };
LDParse::Lexer<LDParse::ErrF> lexer(boundaries_stream, boundary_error); LDParse::ModelStream models; const auto slash = path.find_last_of("/\\"); const auto basename = slash == std::string::npos ? path : path.substr(slash + 1); const auto dot = basename.find_last_of('.'); const auto stem = dot == std::string::npos ? basename : basename.substr(0, dot); std::string root_name = "pm77-" + stem; lexer.lexModelBoundaries(models, root_name);
// Count geometry directly from tokenized model stream size_t t_tri = 0, t_quad = 0, t_line = 0, t_cond = 0, t_incl = 0; std::vector<std::string> tri_positions; for (const auto& [modelName, lineStream] : models) { for (const auto& [lineText, tokens] : lineStream) { if (tokens.empty()) continue; switch (tokens[0].k) { case LDParse::One: t_incl++; break; case LDParse::Two: t_line++; break; case LDParse::Three: { t_tri++; std::string coords = "["; for (size_t ti = 2; ti + 2 < tokens.size() && ti < 11; ti += 3) { if (ti > 2) coords += ","; coords += "["; for (size_t ci = 0; ci < 3; ++ci) { if (ci) coords += ","; const auto* f = std::get_if<float>(&tokens[ti + ci].v); const auto* i = std::get_if<int32_t>(&tokens[ti + ci].v); if (f) coords += std::to_string(*f); else if (i) coords += std::to_string(static_cast<float>(*i)); else coords += "0"; } coords += "]"; } coords += "]"; tri_positions.push_back(coords); break; } case LDParse::Four: t_quad++; break; case LDParse::Five: t_cond++; break; default: break; } } }
std::vector<std::string> names; names.reserve(models.size()); for (const auto& m : models) names.push_back(m.first);
LDParse::ColorTable colors; std::istringstream parse_stream(bytes); LDParse::ModelBuilder<LDParse::ErrF> builder(boundary_error); LDParse::Model* root = builder.construct(path, root_name, parse_stream, colors);
if (!root) { std::cout << error_json(path, "construct_null", "ModelBuilder::construct returned nullptr", diagnostics) << '\n'; return 0; }
ResolvedCounts resolved{t_tri, t_quad, t_line, t_cond, t_incl}; if (const char* ldraw_dir = std::getenv("LDRAWDIR"); ldraw_dir && *ldraw_dir) { std::set<std::string> model_names; for (const auto& model : models) model_names.insert(lowercase(model.first)); std::set<std::string> visited; resolve_includes(ldraw_dir, models, model_names, visited, resolved, boundary_error); }
std::cout << success_json(path, names, *root, diagnostics, t_tri, t_quad, t_line, t_cond, t_incl, resolved, tri_positions) << '\n'; delete root; return 0;}
} // namespace
int main(int argc, char** argv) { if (argc < 2) { std::cerr << "usage: ldraw-oracle FIXTURE...\n"; return 2; } int status = 0; for (int i = 1; i < argc; ++i) status |= run_one(argv[i]); return status;}