Something went wrong. Try again.
atproto Thingiverse but good
Something went wrong. Try again.
24 kB · 626 lines
C++
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627#include <LDParse/Color.hpp>#include <LDParse/Lex.hpp>#include <LDParse/ModelBuilder/ModelBuilderDefs.hpp>#include <LDParse/ModelBuilder/ModelBuilderImpl.hpp>
#include <algorithm>#include <cctype>#include <cmath>#include <cstdlib>#include <exception>#include <filesystem>#include <fstream>#include <iomanip>#include <iostream>#include <limits>#include <map>#include <optional>#include <set>#include <sstream>#include <string>#include <unordered_set>#include <variant>#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) + "\""; }
constexpr const char* ORACLE_SCHEMA = "ldraw-oracle-v4";constexpr const char* ORACLE_WRAPPER_SCHEMA = "wrapper-v4";constexpr const char* ORACLE_PROJECTION_RULES = "pm-78-oracle-only-v4";constexpr const char* ORACLE_REVISION = "0fe78dcee22982f26cceced1b483340faa4d04e9";constexpr const char* ORACLE_PATCH_IDENTITY = "ldparse-oracle-access.patch@pm-78-v2";
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));}
std::optional<std::string> detect_mpd_file_cycle(const std::string& bytes) { std::istringstream input(bytes); std::string line; std::string active_file; while (std::getline(input, line)) { if (!line.empty() && line.back() == '\\r') line.pop_back(); if (line.rfind("0 FILE ", 0) == 0) { active_file = line.substr(7); while (!active_file.empty() && std::isspace(static_cast<unsigned char>(active_file.back()))) active_file.pop_back(); continue; } if (line.rfind("0 NOFILE", 0) == 0) { active_file.clear(); continue; } if (active_file.empty() || line.rfind("1 ", 0) != 0) continue; std::istringstream type1(line); std::string token; std::string target; while (type1 >> token) target = token; if (target == active_file) return active_file; } return std::nullopt;}
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;}
using ModelCounts = std::map<std::string, Counts>;
void add_model_local_counts(ResolvedCounts& counts, const LDParse::Model& model, const ModelCounts& local_counts) { const auto found = local_counts.find(model.name()); if (found != local_counts.end()) add_counts(counts, found->second);}
void resolve_model_occurrences(const LDParse::Model& model, const ModelCounts& local_counts, std::set<const LDParse::Model*>& active, ResolvedCounts& counts) { if (!active.insert(&model).second) return; add_model_local_counts(counts, model, local_counts); for (const auto& [index, bfc, transform, child] : model.children()) { (void)index; (void)bfc; (void)transform; if (child) resolve_model_occurrences(*child, local_counts, active, counts); } active.erase(&model);}
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 float_json(float value);
std::string token_kind_name(LDParse::TokenKind kind) { switch (kind) { case LDParse::Zero: return "zero"; case LDParse::One: return "one"; case LDParse::Two: return "two"; case LDParse::Three: return "three"; case LDParse::Four: return "four"; case LDParse::Five: return "five"; case LDParse::Step: return "step"; case LDParse::Pause: return "pause"; case LDParse::Write: return "write"; case LDParse::Clear: return "clear"; case LDParse::Save: return "save"; case LDParse::Colour: return "colour"; case LDParse::Code: return "code"; case LDParse::Value: return "value"; case LDParse::Edge: return "edge"; case LDParse::Alpha: return "alpha"; case LDParse::Luminance: return "luminance"; case LDParse::Chrome: return "chrome"; case LDParse::Pearlescent: return "pearlescent"; case LDParse::Rubber: return "rubber"; case LDParse::MatteMetallic: return "matte_metallic"; case LDParse::Metal: return "metal"; case LDParse::Material: return "material"; case LDParse::File: return "file"; case LDParse::NoFile: return "no_file"; case LDParse::BFC: return "bfc"; case LDParse::Certify: return "certify"; case LDParse::NoCertify: return "no_certify"; case LDParse::Clip: return "clip"; case LDParse::NoClip: return "no_clip"; case LDParse::InvertNext: return "invert_next"; case LDParse::Orientation: return "orientation"; case LDParse::HexInt: return "hex_int"; case LDParse::DecInt: return "dec_int"; case LDParse::Float: return "float"; case LDParse::Ident: return "ident"; case LDParse::Garbage: return "garbage"; case LDParse::T_EOF: return "eof"; default: return "unknown"; }}
std::string token_value_json(const LDParse::TokenValue& value) { return std::visit([](const auto& item) -> std::string { using Value = std::decay_t<decltype(item)>; if constexpr (std::is_same_v<Value, const char*>) return quote(item ? item : ""); if constexpr (std::is_same_v<Value, std::string>) return quote(item); if constexpr (std::is_same_v<Value, LDParse::Winding>) return item ? "\"cw\"" : "\"ccw\""; if constexpr (std::is_same_v<Value, float>) return float_json(item); if constexpr (std::is_same_v<Value, int32_t>) return std::to_string(item); return "null"; }, value);}
std::string token_json(const LDParse::Token& token) { return "{\"kind\":" + quote(token_kind_name(token.k)) + ",\"kind_code\":" + std::to_string(static_cast<int32_t>(token.k)) + ",\"value\":" + token_value_json(token.v) + ",\"line\":" + std::to_string(token.l) + ",\"column\":" + std::to_string(token.c) + "}";}
std::string tokenized_models_json(const LDParse::ModelStream& models) { std::ostringstream out; out << '['; for (size_t model_index = 0; model_index < models.size(); ++model_index) { if (model_index) out << ','; const auto& [model_name, lines] = models[model_index]; out << "{\"model_index\":" << model_index << ",\"name\":" << quote(model_name) << ",\"lines\":["; for (size_t line_index = 0; line_index < lines.size(); ++line_index) { if (line_index) out << ','; const auto& [text, tokens] = lines[line_index]; out << "{\"index\":" << line_index << ",\"text\":" << quote(text) << ",\"tokens\":["; for (size_t token_index = 0; token_index < tokens.size(); ++token_index) { if (token_index) out << ','; out << token_json(tokens[token_index]); } out << "]}"; } out << "]}"; } return out.str() + ']';}
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 float_json(float value) { if (!std::isfinite(value)) return "null"; std::ostringstream out; out << std::setprecision(std::numeric_limits<float>::max_digits10) << value; return out.str();}
std::string position_json(const LDParse::Position& p) { return "[" + float_json(std::get<0>(p)) + "," + float_json(std::get<1>(p)) + "," + float_json(std::get<2>(p)) + "]";}
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 << position_json(positions[i]); } out << ']'; out << ",\"normal_count\":" << normals.size(); out << ",\"normals\":["; for (size_t i = 0; i < normals.size(); ++i) { if (i) out << ','; out << position_json(normals[i]); } out << ']'; out << ",\"indices\":["; for (size_t i = 0; i < mesh.indices.size(); ++i) { if (i) out << ','; out << mesh.indices[i]; } out << ']'; out << ",\"bf_indices\":["; for (size_t i = 0; i < mesh.bfIndices.size(); ++i) { if (i) out << ','; out << mesh.bfIndices[i]; } out << ']'; out << ",\"triangle_count\":" << (mesh.indices.size() / 3) << ",\"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") << ",\"child_path\":" << (childPtr ? quote(childPtr->getPath()) : "null") << '}'; } return out.str() + ']';}
std::string model_json(const LDParse::Model& model, size_t model_index) { 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()) << ",\"model_index\":" << model_index << ",\"path\":" << quote(model.getPath()) << ",\"certify\":" << quote(certification(model.certify())) << ",\"winding\":\"" << winding_name(model.winding()) << "\"" << ",\"default_color\":" << model.defaultColor() << ",\"step_ends\":["; for (size_t i = 0; i < model.stepEnds().size(); ++i) { if (i) out << ','; out << model.stepEnds()[i]; } out << "]" << ",\"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 all_models_json(const LDParse::Model& root, const std::vector<std::string>& names) { std::ostringstream out; out << '['; const auto cache = root.getSubFileCache(); for (size_t i = 0; i < names.size(); ++i) { if (i) out << ','; const LDParse::Model* model = i == 0 ? &root : nullptr; if (!model && cache) { const auto found = cache->find(names[i]); if (found) model = &found->get(); } if (!model) return "null"; out << model_json(*model, i); } return out.str() + ']';}
std::string success_json(const std::string& path, const std::vector<std::string>& names, const LDParse::Model& root, const LDParse::ModelStream& tokenized_models, 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\":" << quote(ORACLE_SCHEMA) << ",\"oracle_revision\":" << quote(ORACLE_REVISION) << ",\"wrapper_schema_version\":" << quote(ORACLE_WRAPPER_SCHEMA) << ",\"projection_rule_version\":" << quote(ORACLE_PROJECTION_RULES) << ",\"patch_identity\":" << quote(ORACLE_PATCH_IDENTITY) << ",\"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, 0) << ",\"models\":" << all_models_json(root, names) << ",\"tokenized_models\":" << tokenized_models_json(tokenized_models) << ",\"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\":" << quote(ORACLE_SCHEMA) << ",\"oracle_revision\":" << quote(ORACLE_REVISION) << ",\"wrapper_schema_version\":" << quote(ORACLE_WRAPPER_SCHEMA) << ",\"projection_rule_version\":" << quote(ORACLE_PROJECTION_RULES) << ",\"patch_identity\":" << quote(ORACLE_PATCH_IDENTITY) << ",\"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(); if (const auto cycle = detect_mpd_file_cycle(bytes)) { const std::string message = "MPD FILE cycle: " + *cycle; std::vector<Diagnostic> diagnostics{{message, *cycle, true}}; std::cout << error_json(path, "mpd_file_cycle", message, diagnostics) << '\n'; return 0; }
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 += "null"; } 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 = nullptr; { struct CoutRedirect { std::streambuf* old; CoutRedirect() : old(std::cout.rdbuf(std::cerr.rdbuf())) {} ~CoutRedirect() { std::cout.rdbuf(old); } } redirect; 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; }
ModelCounts local_counts; for (const auto& [model_name, line_stream] : models) { LDParse::ModelStream one_model{{model_name, line_stream}}; local_counts.emplace(model_name, count_geometry(one_model)); } ResolvedCounts resolved; std::set<const LDParse::Model*> active; resolve_model_occurrences(*root, local_counts, active, resolved);
std::cout << success_json(path, names, *root, models, 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) { try { status |= run_one(argv[i]); } catch (const std::exception& error) { std::vector<Diagnostic> diagnostics{{"oracle exception", error.what(), true}}; std::cout << error_json(argv[i], "oracle_exception", error.what(), diagnostics) << '\n'; } catch (...) { std::vector<Diagnostic> diagnostics{{"oracle exception", "unknown exception", true}}; std::cout << error_json(argv[i], "oracle_exception", "unknown exception", diagnostics) << '\n'; } } return status;}