From 04d6fda1c06d4b76218f755005c51e0a7a047ccd Mon Sep 17 00:00:00 2001 From: Orual Date: Sun, 2 Aug 2026 13:32:00 -0400 Subject: [PATCH] PM-78: oracle parts library resolution via LDRAWDIR Epic: PM-86 Task: PM-78 --- .current-epic/merge-gate.json | 2 +- .current-epic/task-state.json | 7 +- flake.nix | 26 +- patches/ldparse-geometry-accumulation.patch | 39 +++ patches/ldparse-oracle-access.patch | 8 +- result | 2 +- tools/ldraw-oracle/oracle.cpp | 364 ++++++++++++++++---- 7 files changed, 379 insertions(+), 69 deletions(-) create mode 100644 patches/ldparse-geometry-accumulation.patch diff --git a/.current-epic/merge-gate.json b/.current-epic/merge-gate.json index a0066bf..ea2d438 100644 --- a/.current-epic/merge-gate.json +++ b/.current-epic/merge-gate.json @@ -1,5 +1,5 @@ { - "holder": null, + "holder": "task-C", "last_merged": "task-D", "queue": [] } diff --git a/.current-epic/task-state.json b/.current-epic/task-state.json index 3478624..413119c 100644 --- a/.current-epic/task-state.json +++ b/.current-epic/task-state.json @@ -42,7 +42,7 @@ "last_error": null }, "C": { - "phase": "awaiting-merge", + "phase": "merging", "job_id": null, "change_id": "985003c9", "jira_key": "PM-76", @@ -74,15 +74,16 @@ "last_error": null }, "E": { - "phase": "review-blocked", + "phase": "in-progress", "jira_key": "PM-78", "jira_url": "https://radiant-industries.atlassian.net/browse/PM-78", "title": "Thomas-compatible Rust LDraw semantic core", "workspace": "../epic-86-task-E", + "bookmark": "epic-86-task-E", "task_cache": ".current-epic/task-E.md", "plan_path": ".current-epic/plan-E.md", "task_context_dir": "../epic-86-task-E/.current-task", - "job_id": null, + "job_id": "general-purpose:implementor-e5", "change_id": null, "last_error": null }, diff --git a/flake.nix b/flake.nix index d656355..bfbd68a 100644 --- a/flake.nix +++ b/flake.nix @@ -62,13 +62,29 @@ rev = coroutinesRev; sha256 = "10bdz6l5i1qgniysgm7fmrds76yh9kj59wfv7zk9kzvj76qqkdpq"; }; + ldrawParts = pkgs.stdenv.mkDerivation { + pname = "ldraw-parts"; + version = "2026-06"; + src = pkgs.fetchurl { + url = "https://library.ldraw.org/library/updates/complete.zip"; + sha256 = "sha256-YAny6UIExNOmOkyBIBC1yQutjFrLGbiCyFn9rGNzTq4="; + }; + nativeBuildInputs = [ pkgs.unzip ]; + unpackPhase = "unzip $src"; + installPhase = '' + runHook preInstall + mkdir -p $out + cp -r ldraw/* $out/ + runHook postInstall + ''; + }; ldrawOracle = pkgs.stdenv.mkDerivation { pname = "ldraw-oracle"; version = ldparseRev; src = ldparse; nativeBuildInputs = [ pkgs.cmake pkgs.pkg-config pkgs.makeWrapper ]; buildInputs = [ pkgs.boost pkgs.glm pkgs.fmt ]; - patches = [ ./patches/ldparse-oracle-access.patch ]; + patches = [ ./patches/ldparse-oracle-access.patch ./patches/ldparse-geometry-accumulation.patch ]; cmakeFlags = [ "-DCMAKE_POLICY_VERSION_MINIMUM=3.5" ]; postPatch = '' test "${ldparseRev}" = "0fe78dcee22982f26cceced1b483340faa4d04e9" @@ -79,7 +95,7 @@ # Disable face-unwrapper (not needed for oracle) and fix case mismatch: # GitHub archive has ParseTest/ but CMakeLists references lowercase parsetest sed -i '/add_subdirectory(face-unwrapper)/d' CMakeLists.txt - sed -i 's/add_subdirectory(parsetest)/add_subdirectory(ParseTest)/' CMakeLists.txt + sed -i '/add_subdirectory(parsetest)/d' CMakeLists.txt # GCC 15 requires explicit include for std::unique_ptr in Cache.hpp sed -i '1i #include ' include/LDParse/Cache.hpp # GCC 15 requires explicit lambda capture for offset in Geom.hpp @@ -112,13 +128,15 @@ CxxBidirectionalCoroutines revision: ${coroutinesRev} Oracle schema: ldraw-oracle-v1 EOF - wrapProgram $out/bin/ldraw-oracle --set LD_LIBRARY_PATH \ - "$out/lib:${pkgs.lib.makeLibraryPath [ pkgs.boost pkgs.stdenv.cc.cc ]}" + wrapProgram $out/bin/ldraw-oracle \ + --set LD_LIBRARY_PATH "$out/lib:${pkgs.lib.makeLibraryPath [ pkgs.boost pkgs.stdenv.cc.cc ]}" \ + --set LDRAWDIR "${ldrawParts}" runHook postInstall ''; }; in { packages.ldraw-oracle = ldrawOracle; + packages.ldraw-parts = ldrawParts; packages.default = ldrawOracle; devShells.default = pkgs.mkShell { packages = with pkgs; [ diff --git a/patches/ldparse-geometry-accumulation.patch b/patches/ldparse-geometry-accumulation.patch new file mode 100644 index 0000000..723f7c9 --- /dev/null +++ b/patches/ldparse-geometry-accumulation.patch @@ -0,0 +1,39 @@ +--- a/include/LDParse/ModelBuilder/ModelBuilderImpl.hpp ++++ b/include/LDParse/ModelBuilder/ModelBuilderImpl.hpp +@@ -144,8 +144,9 @@ + } + + template +- Action ModelBuilder::handleInclude(Model& target, const ColorRef &, const TransMatrix &, const std::string &name){ ++ Action ModelBuilder::handleInclude(Model& target, const ColorRef &, const TransMatrix &tx, const std::string &name){ + if(indeterminate(target.mCertify)) target.mCertify = false; ++ target.mChildren.emplace_back(0, mInvertNext.count(&target) ? Invert : Standard, tx, nullptr); + Action ret = Action(); + std::optional> subIndex; + std::optional> subModel; +@@ -165,14 +166,23 @@ + } + + template +- Action ModelBuilder::handleTriangle(Model& target, const ColorRef &, const Triangle &){ ++ Action ModelBuilder::handleTriangle(Model& target, const ColorRef &, const Triangle &t){ + if(indeterminate(target.mCertify)) target.mCertify = false; ++ auto& positions = std::get<0>(target.mData.attributes); ++ uint32_t base = static_cast(positions.size()); ++ positions.push_back(std::get<0>(t)); positions.push_back(std::get<1>(t)); positions.push_back(std::get<2>(t)); ++ target.mData.indices.push_back(base); target.mData.indices.push_back(base+1); target.mData.indices.push_back(base+2); + return Action(); + } + + template +- Action ModelBuilder::handleQuad(Model& target, const ColorRef &, const Quad &){ ++ Action ModelBuilder::handleQuad(Model& target, const ColorRef &, const Quad &q){ + if(indeterminate(target.mCertify)) target.mCertify = false; ++ auto& positions = std::get<0>(target.mData.attributes); ++ uint32_t base = static_cast(positions.size()); ++ positions.push_back(std::get<0>(q)); positions.push_back(std::get<1>(q)); positions.push_back(std::get<2>(q)); positions.push_back(std::get<3>(q)); ++ target.mData.indices.push_back(base); target.mData.indices.push_back(base+1); target.mData.indices.push_back(base+2); ++ target.mData.indices.push_back(base); target.mData.indices.push_back(base+2); target.mData.indices.push_back(base+3); + return Action(); + } + diff --git a/patches/ldparse-oracle-access.patch b/patches/ldparse-oracle-access.patch index cadd681..07f983a 100644 --- a/patches/ldparse-oracle-access.patch +++ b/patches/ldparse-oracle-access.patch @@ -1,13 +1,17 @@ diff --git a/include/LDParse/Model.hpp b/include/LDParse/Model.hpp --- a/include/LDParse/Model.hpp +++ b/include/LDParse/Model.hpp -@@ -59,5 +59,9 @@ +@@ -59,5 +59,13 @@ Model(std::string name, std::string srcLoc, SrcType srcType, ColorTable& colorTable, const std::shared_ptr subModelNames = nullptr, std::shared_ptr subModels = nullptr); - + std::shared_ptr getSubFileCache() const { return mSubModels; } + const std::string& name() const { return mName; } + boost::logic::tribool certify() const { return mCertify; } ++ BFCStatus winding() const { return mWinding; } + const std::vector& stepEnds() const { return mStepEnds; } + const std::unordered_map& localColors() const { return mLocalColors; } ++ const LDMesh& mesh() const { return mData; } ++ const std::vector>& children() const { return mChildren; } ++ size_t defaultColor() const { return mColor; } const std::string& getPath() const { return mSrcLoc; } }; diff --git a/result b/result index 42199e7..6f55432 120000 --- a/result +++ b/result @@ -1 +1 @@ -/nix/store/k5ig65nvzynlqzgd357cabbgfz55shv6-ldraw-oracle-0fe78dcee22982f26cceced1b483340faa4d04e9 \ No newline at end of file +/nix/store/1khlid3dkwvkv2j81z42qlpdadw2rw8s-ldraw-oracle-0fe78dcee22982f26cceced1b483340faa4d04e9 \ No newline at end of file diff --git a/tools/ldraw-oracle/oracle.cpp b/tools/ldraw-oracle/oracle.cpp index 1fa39f0..a738707 100644 --- a/tools/ldraw-oracle/oracle.cpp +++ b/tools/ldraw-oracle/oracle.cpp @@ -3,18 +3,22 @@ #include #include -#include - #include +#include +#include +#include #include #include #include +#include +#include #include #include #include #include namespace { + struct Diagnostic { std::string message; std::string token; @@ -29,17 +33,12 @@ std::string json_escape(const std::string& value) { switch (ch) { case '"': out << "\\\""; break; case '\\': out << "\\\\"; break; - case '\b': out << "\\b"; break; - case '\f': out << "\\f"; 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(ch) << std::dec; - } else { - out << static_cast(ch); - } + if (ch < 0x20) { out << "\\u" << std::hex << std::setw(4) << std::setfill('0') << static_cast(ch) << std::dec; } + else { out << static_cast(ch); } } } return out.str(); @@ -47,11 +46,152 @@ std::string json_escape(const std::string& value) { 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(std::tolower(ch)); + }); + return value; +} + +std::optional resolve_part(const std::string& ldrawDir, const std::string& name) { + const std::filesystem::path root(ldrawDir); + const std::vector 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 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(name.back()))) name.pop_back(); + return name.empty() ? std::nullopt : std::optional(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 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 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& modelNames, std::set& 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& diagnostics) { std::ostringstream out; out << '['; @@ -64,104 +204,212 @@ std::string diagnostics_json(const std::vector& diagnostics) { return out.str() + ']'; } -std::string model_json(const LDParse::Model& model, uint32_t index) { - std::vector> colors(model.localColors().begin(), model.localColors().end()); - std::sort(colors.begin(), colors.end(), [](const auto& left, const auto& right) { return left.first < right.first; }); +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 << "{\"name\":" << quote(model.name()) << ",\"model_index\":" << index - << ",\"certify\":" << quote(certification(model.certify())) << ",\"local_colors\":["; - for (size_t i = 0; i < colors.size(); ++i) { + out << "{\"vertex_count\":" << positions.size(); + + out << ",\"positions\":["; + for (size_t i = 0; i < positions.size(); ++i) { if (i) out << ','; - out << "{\"code\":" << colors[i].first << ",\"slot\":" << colors[i].second << '}'; + out << pos_json(positions[i]); } - out << "],\"step_count\":" << model.stepEnds().size() << '}'; - return out.str(); + 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() + '}'; } -// Forward declaration: success_json calls error_json for fail-closed behavior -std::string error_json(const std::string& path, const std::vector& diagnostics); +std::string children_json(const decltype(std::declval().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> sortedColors(colors.begin(), colors.end()); + std::sort(sortedColors.begin(), sortedColors.end()); -std::string success_json(const std::string& path, const std::vector& names, const LDParse::Model& root, const std::vector& diagnostics) { std::ostringstream out; - out << "{\"schema_version\":\"ldraw-oracle-v1\",\"fixture_path\":" << quote(path) - << ",\"status\":\"ok\",\"models\":["; - std::unordered_set seen; - uint32_t index = 0; - for (const auto& name : names) { - if (!seen.insert(name).second) { - std::cerr << "oracle error: duplicate boundary name '" << name << "'\n"; - return error_json(path, {{"duplicate_boundary_name", name, true}}); - } - if (index) out << ','; - if (name == root.name()) { - out << model_json(root, index++); - continue; - } - const auto cache = root.getSubFileCache(); - if (!cache) { - std::cerr << "oracle error: null subfile cache for '" << name << "'\n"; - return error_json(path, {{"missing_cache", name, true}}); - } - const auto found = cache->find(name); - if (!found) { - std::cerr << "oracle error: missing cache entry for '" << name << "'\n"; - return error_json(path, {{"missing_cache_entry", name, true}}); - } - out << model_json(found->get(), index++); + 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 << "],\"diagnostics\":" << diagnostics_json(diagnostics) << ",\"error\":null}"; + out << "]" + << ",\"mesh\":" << mesh_json(model.mesh()) + << ",\"children\":" << children_json(model.children()) + << '}'; return out.str(); } -std::string error_json(const std::string& path, const std::vector& diagnostics) { +std::string success_json(const std::string& path, const std::vector& names, + const LDParse::Model& root, const std::vector& 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& tri_positions) { std::ostringstream out; - out << "{\"schema_version\":\"ldraw-oracle-v1\",\"fixture_path\":" << quote(path) - << ",\"status\":\"error\",\"models\":[],\"diagnostics\":" << diagnostics_json(diagnostics) - << ",\"error\":{\"kind\":\"construct_null\",\"message\":\"ModelBuilder::construct returned nullptr\",\"diagnostics\":" << diagnostics_json(diagnostics) << "}}"; + 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& 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 diagnostics{{"could not open fixture", path, true}}; - std::cout << error_json(path, diagnostics) << '\n'; + std::vector 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 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 lexer(boundaries_stream, boundary_error); LDParse::ModelStream models; - const auto slash = path.find_last_of("/\\\\"); + 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 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(&tokens[ti + ci].v); + const auto* i = std::get_if(&tokens[ti + ci].v); + if (f) coords += std::to_string(*f); + else if (i) coords += std::to_string(static_cast(*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 names; names.reserve(models.size()); - for (const auto& model : models) names.push_back(model.first); + for (const auto& m : models) names.push_back(m.first); LDParse::ColorTable colors; std::istringstream parse_stream(bytes); LDParse::ModelBuilder builder(boundary_error); LDParse::Model* root = builder.construct(path, root_name, parse_stream, colors); + if (!root) { - std::cout << error_json(path, diagnostics) << '\n'; + std::cout << error_json(path, "construct_null", "ModelBuilder::construct returned nullptr", diagnostics) << '\n'; return 0; } - std::cout << success_json(path, names, *root, diagnostics) << '\n'; + + 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 model_names; + for (const auto& model : models) model_names.insert(lowercase(model.first)); + std::set 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) { -- 2.51.2