From 4d0af82aab486d3887c0ce8dceb7e59e70481959 Mon Sep 17 00:00:00 2001 From: sladro Date: Fri, 24 Apr 2026 09:47:27 +0800 Subject: [PATCH] Add configurable GLB node naming --- CMakeLists.txt | 3 +- desktop/tmp-render.ts | 4 + src/cadit/occt/convert.cpp | 6 +- src/cadit/occt/debug.cpp | 2 +- src/cadit/occt/node_naming.cpp | 306 +++++++++++++++++++++++++++++++++ src/cadit/occt/node_naming.h | 32 ++++ src/cadit/occt/step_tree.cpp | 38 +++- src/cadit/occt/step_tree.h | 9 +- src/config_structs.h | 9 + src/config_utils.cpp | 3 + src/http_server.cpp | 11 ++ src/main.cpp | 22 +++ tests/check_node_names.ps1 | 31 ++++ tests/tests.cmake | 26 ++- 14 files changed, 490 insertions(+), 12 deletions(-) create mode 100644 desktop/tmp-render.ts create mode 100644 src/cadit/occt/node_naming.cpp create mode 100644 src/cadit/occt/node_naming.h create mode 100644 tests/check_node_names.ps1 diff --git a/CMakeLists.txt b/CMakeLists.txt index c1d1371..7d9a71c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -83,6 +83,7 @@ set(SOURCES src/cadit/occt/debug.cpp src/cadit/occt/gltf_writer.cpp src/cadit/occt/convert.cpp + src/cadit/occt/node_naming.cpp src/cadit/occt/step_helpers.cpp src/cadit/occt/bsplinesurf.cpp src/cadit/occt/helpers.cpp @@ -98,6 +99,7 @@ set(HEADERS src/geom/Color.h src/cadit/occt/step_tree.h src/cadit/occt/convert.h + src/cadit/occt/node_naming.h src/cadit/occt/debug.h src/cadit/occt/step_helpers.h src/cadit/occt/gltf_writer.h @@ -142,4 +144,3 @@ if (BUILD_TESTING) enable_testing() include(tests/tests.cmake) endif (BUILD_TESTING) - diff --git a/desktop/tmp-render.ts b/desktop/tmp-render.ts new file mode 100644 index 0000000..de88fc8 --- /dev/null +++ b/desktop/tmp-render.ts @@ -0,0 +1,4 @@ +import { renderToStaticMarkup } from "react-dom/server"; +import App from "./src/App"; + +console.log(renderToStaticMarkup()); diff --git a/src/cadit/occt/convert.cpp b/src/cadit/occt/convert.cpp index d18e8de..83e40be 100644 --- a/src/cadit/occt/convert.cpp +++ b/src/cadit/occt/convert.cpp @@ -29,6 +29,7 @@ #include "custom_progress.h" #include "geometry_iterator.h" +#include "node_naming.h" #include "step_helpers.h" #include "step_tree.h" #include "../../config_structs.h" @@ -262,6 +263,9 @@ void convert_stp_to_glb(const GlobalConfig& config) std::cout << "Reading STEP file: " << config.stpFile << std::endl; if (reader.ReadFile(config.stpFile.string().c_str()) != IFSelect_RetDone) throw std::runtime_error("Error reading STEP file"); + auto default_reader = reader.ChangeReader(); + auto model = default_reader.StepModel(); + Interface_Graph theGraph(model, /*keepTransient*/ Standard_False); auto stop = std::chrono::high_resolution_clock::now(); auto duration = std::chrono::duration(stop - start).count(); @@ -334,6 +338,7 @@ void convert_stp_to_glb(const GlobalConfig& config) throw std::runtime_error("Error writing GLB file"); } + apply_node_name_mode_to_glb(config.glbFile, model, theGraph, config.nodeNameMode); apply_world_translation_to_glb(config.glbFile, root_translation); stop = std::chrono::high_resolution_clock::now(); duration = std::chrono::duration(stop - start).count(); @@ -354,4 +359,3 @@ void convert_stp_to_glb(const GlobalConfig& config) log_file << "]\n"; log_file.close(); } - diff --git a/src/cadit/occt/debug.cpp b/src/cadit/occt/debug.cpp index c170be7..5805258 100644 --- a/src/cadit/occt/debug.cpp +++ b/src/cadit/occt/debug.cpp @@ -106,7 +106,7 @@ void debug_stp_to_glb(const GlobalConfig &config) { std::cout << "Number of entities: " << num_entities << "\n"; // Extract hierarchy - auto roots = ExtractProductHierarchy(model, theGraph); + auto roots = ExtractProductHierarchy(model, theGraph, config.nodeNameMode); add_geometries_to_nodes(roots, theGraph); auto step_store = StepStore(roots); diff --git a/src/cadit/occt/node_naming.cpp b/src/cadit/occt/node_naming.cpp new file mode 100644 index 0000000..c3884f0 --- /dev/null +++ b/src/cadit/occt/node_naming.cpp @@ -0,0 +1,306 @@ +#include "node_naming.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "step_tree.h" + +namespace { + +std::string trim_copy(std::string value) +{ + auto not_space = [](unsigned char c) { return !std::isspace(c); }; + value.erase(value.begin(), std::find_if(value.begin(), value.end(), not_space)); + value.erase(std::find_if(value.rbegin(), value.rend(), not_space).base(), value.end()); + return value; +} + +std::string ascii_lower(std::string value) +{ + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) { + return static_cast(std::tolower(c)); + }); + return value; +} + +std::string to_string(const Handle(TCollection_HAsciiString)& value) +{ + if (value.IsNull()) { + return {}; + } + return trim_copy(value->ToCString()); +} + +std::string title_with_version(const NodeNameParts& parts) +{ + if (parts.title.empty()) { + return parts.version; + } + if (parts.version.empty()) { + return parts.title; + } + return parts.title + " " + parts.version; +} + +void write_updated_glb_json(const std::filesystem::path& glb_path, + const std::vector& data, + const std::size_t json_data_offset, + const std::uint32_t old_json_length, + const nlohmann::json& document) +{ + constexpr std::uint32_t kJsonChunkType = 0x4E4F534A; + + std::string updated_json = document.dump(); + const std::size_t padded_json_length = (updated_json.size() + 3) & ~std::size_t(3); + updated_json.resize(padded_json_length, ' '); + + const std::size_t json_chunk_total_size = sizeof(std::uint32_t) * 2 + padded_json_length; + const std::size_t remaining_size = data.size() - (json_data_offset + old_json_length); + const std::size_t new_file_size = sizeof(std::uint32_t) * 3 + json_chunk_total_size + remaining_size; + + std::vector output; + output.reserve(new_file_size); + output.insert(output.end(), data.begin(), data.begin() + sizeof(std::uint32_t) * 3); + + const std::uint32_t new_length = static_cast(new_file_size); + std::memcpy(output.data() + sizeof(std::uint32_t) * 2, &new_length, sizeof(std::uint32_t)); + + const std::uint32_t new_json_length = static_cast(padded_json_length); + const char* json_length_bytes = reinterpret_cast(&new_json_length); + output.insert(output.end(), json_length_bytes, json_length_bytes + sizeof(std::uint32_t)); + + const char* chunk_type_bytes = reinterpret_cast(&kJsonChunkType); + output.insert(output.end(), chunk_type_bytes, chunk_type_bytes + sizeof(std::uint32_t)); + + output.insert(output.end(), updated_json.begin(), updated_json.end()); + output.insert(output.end(), data.begin() + json_data_offset + old_json_length, data.end()); + + std::ofstream out_file(glb_path, std::ios::binary | std::ios::trunc); + if (!out_file) { + throw std::runtime_error("Failed to open GLB for node name update: " + glb_path.string()); + } + out_file.write(output.data(), static_cast(output.size())); +} + +int default_scene_index(const nlohmann::json& document) +{ + if (document.contains("scene") && document["scene"].is_number_integer()) { + return document["scene"].get(); + } + return 0; +} + +bool apply_product_name_tree_to_gltf_node(nlohmann::json& document, + int node_index, + const ProductNode& product_node) +{ + if (!document.contains("nodes") || !document["nodes"].is_array() || + node_index < 0 || node_index >= static_cast(document["nodes"].size())) { + return false; + } + + bool changed = false; + auto& gltf_node = document["nodes"][node_index]; + if (!product_node.name.empty() && + (!gltf_node.contains("name") || !gltf_node["name"].is_string() || + gltf_node["name"].get() != product_node.name)) { + gltf_node["name"] = product_node.name; + changed = true; + } + + if (!gltf_node.contains("children") || !gltf_node["children"].is_array()) { + return changed; + } + + const auto& children = gltf_node["children"]; + const std::size_t count = std::min(children.size(), product_node.children.size()); + if (children.size() != product_node.children.size()) { + std::cerr << "Warning: GLB node child count does not match STEP hierarchy for node: " + << product_node.name << "\n"; + } + + for (std::size_t i = 0; i < count; ++i) { + if (!children[i].is_number_integer() || !product_node.children[i]) { + continue; + } + changed = apply_product_name_tree_to_gltf_node(document, + children[i].get(), + *product_node.children[i]) || changed; + } + + return changed; +} + +bool apply_product_name_tree_to_gltf(nlohmann::json& document, + const std::vector>& roots) +{ + if (roots.empty() || !document.contains("scenes") || !document["scenes"].is_array()) { + return false; + } + + const int scene_index = default_scene_index(document); + if (scene_index < 0 || scene_index >= static_cast(document["scenes"].size())) { + return false; + } + + auto& scene = document["scenes"][scene_index]; + if (!scene.contains("nodes") || !scene["nodes"].is_array()) { + return false; + } + + const auto& scene_nodes = scene["nodes"]; + const std::size_t count = std::min(scene_nodes.size(), roots.size()); + if (scene_nodes.size() != roots.size()) { + std::cerr << "Warning: GLB root node count does not match STEP hierarchy\n"; + } + + bool changed = false; + for (std::size_t i = 0; i < count; ++i) { + if (!scene_nodes[i].is_number_integer() || !roots[i]) { + continue; + } + changed = apply_product_name_tree_to_gltf_node(document, + scene_nodes[i].get(), + *roots[i]) || changed; + } + + return changed; +} + +} // namespace + +NodeNameMode parse_node_name_mode(const std::string& value) +{ + const std::string normalized = ascii_lower(trim_copy(value)); + if (normalized.empty() || normalized == "source") { + return NodeNameMode::Source; + } + if (normalized == "instance") { + return NodeNameMode::Instance; + } + if (normalized == "title") { + return NodeNameMode::Title; + } + if (normalized == "title-version" || normalized == "title_version") { + return NodeNameMode::TitleVersion; + } + if (normalized == "combined") { + return NodeNameMode::Combined; + } + throw std::invalid_argument("Invalid --node-name-mode value: " + value); +} + +std::string format_node_name(NodeNameMode mode, const NodeNameParts& parts) +{ + const std::string title_version = title_with_version(parts); + + switch (mode) { + case NodeNameMode::Source: + return {}; + case NodeNameMode::Instance: + return !parts.instance_title.empty() ? parts.instance_title : title_version; + case NodeNameMode::Title: + return !parts.title.empty() ? parts.title : (!parts.instance_title.empty() ? parts.instance_title : parts.version); + case NodeNameMode::TitleVersion: + return !title_version.empty() ? title_version : parts.instance_title; + case NodeNameMode::Combined: + if (!parts.instance_title.empty() && !parts.title.empty() && parts.instance_title != parts.title) { + return parts.instance_title + " (" + parts.title + ")"; + } + return !parts.instance_title.empty() ? parts.instance_title : title_version; + } + + return {}; +} + +NodeNameParts make_node_name_parts(const Handle(StepBasic_Product)& product, + const Handle(StepBasic_ProductDefinition)& product_definition, + const Handle(StepRepr_NextAssemblyUsageOccurrence)& occurrence) +{ + NodeNameParts parts; + if (!occurrence.IsNull()) { + parts.instance_title = to_string(occurrence->Id()); + if (parts.instance_title.empty()) { + parts.instance_title = to_string(occurrence->Name()); + } + if (parts.instance_title.empty() && occurrence->HasReferenceDesignator()) { + parts.instance_title = to_string(occurrence->ReferenceDesignator()); + } + } + + if (!product.IsNull()) { + parts.title = to_string(product->Name()); + } + + if (!product_definition.IsNull() && !product_definition->Formation().IsNull()) { + parts.version = to_string(product_definition->Formation()->Id()); + } + + return parts; +} + +void apply_node_name_mode_to_glb(const std::filesystem::path& glb_path, + const Handle(Interface_InterfaceModel)& model, + const Interface_Graph& graph, + NodeNameMode mode) +{ + if (mode == NodeNameMode::Source || model.IsNull()) { + return; + } + + constexpr std::uint32_t kGlbMagic = 0x46546C67; + constexpr std::uint32_t kJsonChunkType = 0x4E4F534A; + + std::ifstream input(glb_path, std::ios::binary); + if (!input) { + throw std::runtime_error("Failed to open GLB for node name update: " + glb_path.string()); + } + + input.seekg(0, std::ios::end); + const std::streamsize file_size = input.tellg(); + input.seekg(0, std::ios::beg); + + std::vector data(static_cast(file_size)); + input.read(data.data(), file_size); + + auto read_u32 = [](const char* ptr) { + std::uint32_t value; + std::memcpy(&value, ptr, sizeof(std::uint32_t)); + return value; + }; + + if (data.size() < sizeof(std::uint32_t) * 5 || read_u32(data.data()) != kGlbMagic) { + throw std::runtime_error("Invalid GLB file for node name update: " + glb_path.string()); + } + + const std::size_t json_chunk_header_offset = sizeof(std::uint32_t) * 3; + const std::uint32_t json_length = read_u32(data.data() + json_chunk_header_offset); + const std::uint32_t json_type = read_u32(data.data() + json_chunk_header_offset + sizeof(std::uint32_t)); + if (json_type != kJsonChunkType) { + throw std::runtime_error("Unexpected GLB JSON chunk for node name update: " + glb_path.string()); + } + + const std::size_t json_data_offset = json_chunk_header_offset + sizeof(std::uint32_t) * 2; + if (json_data_offset + json_length > data.size()) { + throw std::runtime_error("Invalid GLB JSON length for node name update: " + glb_path.string()); + } + + const std::string json_text(data.data() + json_data_offset, data.data() + json_data_offset + json_length); + nlohmann::json document = nlohmann::json::parse(json_text); + + auto roots = ExtractProductHierarchy(model, graph, mode); + const bool changed = apply_product_name_tree_to_gltf(document, roots); + + if (changed) { + write_updated_glb_json(glb_path, data, json_data_offset, json_length, document); + } +} diff --git a/src/cadit/occt/node_naming.h b/src/cadit/occt/node_naming.h new file mode 100644 index 0000000..9493900 --- /dev/null +++ b/src/cadit/occt/node_naming.h @@ -0,0 +1,32 @@ +#ifndef STP2GLB_NODE_NAMING_H +#define STP2GLB_NODE_NAMING_H + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "../../config_structs.h" + +struct NodeNameParts { + std::string instance_title; + std::string title; + std::string version; +}; + +NodeNameMode parse_node_name_mode(const std::string& value); +std::string format_node_name(NodeNameMode mode, const NodeNameParts& parts); +NodeNameParts make_node_name_parts(const Handle(StepBasic_Product)& product, + const Handle(StepBasic_ProductDefinition)& product_definition, + const Handle(StepRepr_NextAssemblyUsageOccurrence)& occurrence); +void apply_node_name_mode_to_glb(const std::filesystem::path& glb_path, + const Handle(Interface_InterfaceModel)& model, + const Interface_Graph& graph, + NodeNameMode mode); + +#endif // STP2GLB_NODE_NAMING_H diff --git a/src/cadit/occt/step_tree.cpp b/src/cadit/occt/step_tree.cpp index b19096c..10385ab 100644 --- a/src/cadit/occt/step_tree.cpp +++ b/src/cadit/occt/step_tree.cpp @@ -3,6 +3,7 @@ #include // STEP entity classes +#include #include #include #include @@ -30,6 +31,7 @@ #include #include +#include "node_naming.h" #include "step_helpers.h" @@ -343,7 +345,8 @@ BuildAssemblyLinksWithTransformation(const Handle(Interface_InterfaceModel) &mod int ProductNode::instanceCounter = 0; // Main function: extracts top-level ProductNode trees with transformations std::vector > ExtractProductHierarchy(const Handle(Interface_InterfaceModel) &model, - const Interface_Graph &theGraph) { + const Interface_Graph &theGraph, + NodeNameMode node_name_mode) { // 1) Build the map of parent->children relationships // to be replaced by this const auto parentToChildrenWTransforms = BuildAssemblyLinksWithTransformation(model, theGraph); @@ -362,6 +365,7 @@ std::vector > ExtractProductHierarchy(const Handle( for (auto productIndexMap = BuildProductIndexMap(model); auto &val: productIndexMap | std::views::values) { allProducts.push_back(val); } + std::sort(allProducts.begin(), allProducts.end()); // 4) For each product, if it’s NOT in allChildren => it’s a root std::vector > roots; @@ -369,7 +373,8 @@ std::vector > ExtractProductHierarchy(const Handle( if (!allChildren.contains(idx)) { // This is a root product, start with the identity transformation roots.push_back( - BuildProductNodeWithTransform(idx, parentToChildrenWTransforms, model, theGraph, gp_Trsf())); + BuildProductNodeWithTransform(idx, parentToChildrenWTransforms, model, theGraph, gp_Trsf(), nullptr, + node_name_mode)); } } return roots; @@ -447,7 +452,9 @@ static std::unique_ptr BuildProductNodeWithTransform( const Handle(Interface_InterfaceModel) &model, const Interface_Graph &theGraph, const gp_Trsf &parentTransform, - ProductNode *parent) { + ProductNode *parent, + NodeNameMode node_name_mode, + const ParentChildRelationship* relationship) { auto node = std::make_unique(); node->entityIndex = productIndex; node->parent = parent; @@ -455,10 +462,29 @@ static std::unique_ptr BuildProductNodeWithTransform( const Handle(Standard_Transient) ent = model->Value(productIndex); const auto product = Handle(StepBasic_Product)::DownCast(ent); - if (!product.IsNull() && !product->Name().IsNull()) { + if (node_name_mode != NodeNameMode::Source && !product.IsNull()) { + Handle(StepRepr_NextAssemblyUsageOccurrence) occurrence; + if (relationship != nullptr && relationship->nauoIndex > 0 && relationship->nauoIndex <= model->NbEntities()) { + occurrence = Handle(StepRepr_NextAssemblyUsageOccurrence)::DownCast(model->Value(relationship->nauoIndex)); + } + + Handle(StepBasic_ProductDefinition) product_definition; + if (!occurrence.IsNull()) { + product_definition = occurrence->RelatedProductDefinition(); + } else { + product_definition = FindProductDefinition(product, model, theGraph); + } + + node->name = format_node_name(node_name_mode, + make_node_name_parts(product, product_definition, occurrence)); + } + + if (node->name.empty() && !product.IsNull() && !product->Name().IsNull()) { node->name = product->Name()->ToCString(); } else { - node->name = "(unnamed product)"; + if (node->name.empty()) { + node->name = "(unnamed product)"; + } } // Combine parent transformation with local transformation @@ -477,7 +503,7 @@ static std::unique_ptr BuildProductNodeWithTransform( // Build the child node node->children.push_back( BuildProductNodeWithTransform(childRel.childIndex, parentToChildrenWTransforms, model, theGraph, - childAbsoluteTransform, node.get())); + childAbsoluteTransform, node.get(), node_name_mode, &childRel)); } } diff --git a/src/cadit/occt/step_tree.h b/src/cadit/occt/step_tree.h index 7aae092..6d965e4 100644 --- a/src/cadit/occt/step_tree.h +++ b/src/cadit/occt/step_tree.h @@ -16,6 +16,8 @@ #include #include +#include "../../config_structs.h" + struct ProcessResult { mutable bool added_to_model; mutable std::string skip_reason; @@ -70,7 +72,8 @@ struct ParentChildRelationship { }; std::vector > ExtractProductHierarchy(const Handle(Interface_InterfaceModel) &model, - const Interface_Graph &theGraph); + const Interface_Graph &theGraph, + NodeNameMode node_name_mode = NodeNameMode::Source); std::string ExportHierarchyToJson(const std::vector > &roots); @@ -86,7 +89,9 @@ static std::unique_ptr BuildProductNodeWithTransform( const Handle(Interface_InterfaceModel)& model, const Interface_Graph& theGraph, const gp_Trsf& parentTransform = gp_Trsf(), - ProductNode* parent = nullptr); + ProductNode* parent = nullptr, + NodeNameMode node_name_mode = NodeNameMode::Source, + const ParentChildRelationship* relationship = nullptr); static std::unique_ptr BuildProductNodeWithTransformIterative( int rootIndex, diff --git a/src/config_structs.h b/src/config_structs.h index c37b9ab..9b44811 100644 --- a/src/config_structs.h +++ b/src/config_structs.h @@ -9,6 +9,14 @@ #include #include +enum class NodeNameMode { + Source, + Instance, + Title, + TitleVersion, + Combined +}; + struct BuildConfig { bool build_bspline_surf = false; }; @@ -40,6 +48,7 @@ struct GlobalConfig { std::vector filter_names_include; std::vector filter_names_exclude; + NodeNameMode nodeNameMode = NodeNameMode::Source; BuildConfig buildConfig; ServerConfig serverConfig; diff --git a/src/config_utils.cpp b/src/config_utils.cpp index 3c5850d..08e6e1c 100644 --- a/src/config_utils.cpp +++ b/src/config_utils.cpp @@ -11,6 +11,7 @@ #include "config_structs.h" #include "cadit/occt/helpers.h" +#include "cadit/occt/node_naming.h" #include "CLI/App.hpp" #include "http_downloader.h" @@ -84,6 +85,7 @@ GlobalConfig process_parameters_internal(CLI::App& app, const auto compressed_glb_value = app.get_option("--compressed-glb")->as(); const auto gltfpack_path_value = app.get_option("--gltfpack-path")->as(); const auto gltfpack_args_value = app.get_option("--gltfpack-args")->as(); + const auto node_name_mode_value = app.get_option("--node-name-mode")->as(); // Process include and exclude filter names const auto filter_names_include = process_filter_names(filter_names_include_input, filter_names_file_include); @@ -148,6 +150,7 @@ GlobalConfig process_parameters_internal(CLI::App& app, .tessellation_timout = app.get_option("--tessellation-timeout")->as(), .filter_names_include = filter_names_include, .filter_names_exclude = filter_names_exclude, + .nodeNameMode = parse_node_name_mode(node_name_mode_value), .buildConfig = {}, .serverConfig = { .enable_server = app.get_option("--server")->as(), diff --git a/src/http_server.cpp b/src/http_server.cpp index dbbe349..0e4d4ae 100644 --- a/src/http_server.cpp +++ b/src/http_server.cpp @@ -2,6 +2,7 @@ #include "third_party/httplib.h" #include "cadit/occt/convert.h" #include "cadit/occt/debug.h" +#include "cadit/occt/node_naming.h" #include "http_downloader.h" #include "compression_utils.h" #include @@ -95,6 +96,16 @@ void start_http_server(const GlobalConfig& base_config) { if (req.form.has_field("maxGeometryNum")) { config.max_geometry_num = std::stoi(req.form.get_field("maxGeometryNum")); } + if (req.form.has_field("nodeNameMode")) { + try { + config.nodeNameMode = parse_node_name_mode(req.form.get_field("nodeNameMode")); + } catch (const std::exception& ex) { + res.status = 400; + std::string error_msg = "{\"success\":false,\"error\":\"" + std::string(ex.what()) + "\"}"; + res.set_content(error_msg, "application/json"); + return; + } + } if (req.form.has_field("compressGlb")) { config.compress_glb = req.form.get_field("compressGlb") == "true"; diff --git a/src/main.cpp b/src/main.cpp index 32d1d39..a51b20a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,6 +13,7 @@ #include #include "cadit/occt/debug.h" #include "cadit/occt/convert.h" +#include "cadit/occt/node_naming.h" #include "cadit/occt/bsplinesurf.h" #include "cadit/occt/helpers.h" #include "config_utils.h" @@ -189,6 +190,25 @@ void print_status(const GlobalConfig& config) { std::cout << "Solid Only: " << config.solidOnly << "\n"; std::cout << "Max Geometry Num: " << config.max_geometry_num << "\n"; std::cout << "Tessellation Timeout: " << config.tessellation_timout << "\n\n"; + std::cout << "Node Name Mode: "; + switch (config.nodeNameMode) { + case NodeNameMode::Source: + std::cout << "source"; + break; + case NodeNameMode::Instance: + std::cout << "instance"; + break; + case NodeNameMode::Title: + std::cout << "title"; + break; + case NodeNameMode::TitleVersion: + std::cout << "title-version"; + break; + case NodeNameMode::Combined: + std::cout << "combined"; + break; + } + std::cout << "\n\n"; std::cout << "Compression: " << (config.compress_glb ? "enabled" : "disabled") << "\n"; if (config.compress_glb) { @@ -241,6 +261,7 @@ int main(int argc, char* argv[]) app.add_option("--filter-names-exclude", "Exclude Filter name. Command separated list")->default_val(""); app.add_option("--filter-names-file-exclude", "Exclude Filter name file")->default_val(""); app.add_option("--tessellation-timeout", "Tessellation timeout")->default_val(30); + app.add_option("--node-name-mode", "GLB node names: source, instance, title, title-version, combined")->default_val("source"); app.add_flag("--compress-glb", "Enable glTF compression using gltfpack"); app.add_option("--compressed-glb", "Optional output path for compressed GLB (defaults to original)")->default_val(""); app.add_option("--gltfpack-path", "Path to gltfpack executable")->default_val("gltfpack"); @@ -259,6 +280,7 @@ int main(int argc, char* argv[]) config.solidOnly = false; config.max_geometry_num = 0; config.tessellation_timout = app.get_option("--tessellation-timeout")->as(); + config.nodeNameMode = parse_node_name_mode(app.get_option("--node-name-mode")->as()); config.serverConfig.enable_server = true; config.serverConfig.port = app.get_option("--port")->as(); diff --git a/tests/check_node_names.ps1 b/tests/check_node_names.ps1 new file mode 100644 index 0000000..1027a02 --- /dev/null +++ b/tests/check_node_names.ps1 @@ -0,0 +1,31 @@ +param( + [Parameter(Mandatory = $true)][string]$ExePath, + [Parameter(Mandatory = $true)][string]$StepPath, + [Parameter(Mandatory = $true)][string]$GlbPath, + [Parameter(Mandatory = $true)][string]$NodeNameMode, + [Parameter(Mandatory = $true)][string]$ExpectedNames +) + +$ErrorActionPreference = "Stop" + +& $ExePath --stp $StepPath --glb $GlbPath --node-name-mode $NodeNameMode +if ($LASTEXITCODE -ne 0) { + throw "STP2GLB failed with exit code $LASTEXITCODE" +} + +$bytes = [System.IO.File]::ReadAllBytes($GlbPath) +if ($bytes.Length -lt 20) { + throw "GLB file is too small" +} + +$jsonLength = [System.BitConverter]::ToUInt32($bytes, 12) +$jsonText = [System.Text.Encoding]::UTF8.GetString($bytes, 20, $jsonLength).Trim() +$document = $jsonText | ConvertFrom-Json +$actualNames = @($document.nodes | ForEach-Object { $_.name }) +$expectedNameList = @($ExpectedNames -split '\|') + +foreach ($expectedName in $expectedNameList) { + if ($actualNames -notcontains $expectedName) { + throw "Missing node name '$expectedName'. Actual names: $($actualNames -join ', ')" + } +} diff --git a/tests/tests.cmake b/tests/tests.cmake index 3a70342..efa9075 100644 --- a/tests/tests.cmake +++ b/tests/tests.cmake @@ -35,6 +35,30 @@ add_test(NAME debug_as1_mini COMMAND STP2GLB WORKING_DIRECTORY "${CMAKE_INSTALL_PREFIX}/bin" ) +if(WIN32 AND BUILD_STATIC) + add_test(NAME node_name_title COMMAND powershell + -ExecutionPolicy Bypass + -File ${CMAKE_CURRENT_SOURCE_DIR}/tests/check_node_names.ps1 + -ExePath $ + -StepPath ${CMAKE_CURRENT_SOURCE_DIR}/files/as1-oc-214-mini.stp + -GlbPath ${CMAKE_CURRENT_SOURCE_DIR}/temp/as1-node-title-test.glb + -NodeNameMode title + -ExpectedNames "l-bracket-assembly|nut-bolt-assembly|bolt" + WORKING_DIRECTORY "${CMAKE_INSTALL_PREFIX}/bin" + ) + + add_test(NAME node_name_combined COMMAND powershell + -ExecutionPolicy Bypass + -File ${CMAKE_CURRENT_SOURCE_DIR}/tests/check_node_names.ps1 + -ExePath $ + -StepPath ${CMAKE_CURRENT_SOURCE_DIR}/files/as1-oc-214-mini.stp + -GlbPath ${CMAKE_CURRENT_SOURCE_DIR}/temp/as1-node-combined-test.glb + -NodeNameMode combined + -ExpectedNames "6 (l-bracket-assembly)|2 (nut-bolt-assembly)|1 (bolt)" + WORKING_DIRECTORY "${CMAKE_INSTALL_PREFIX}/bin" + ) +endif() + add_test(NAME debug_as1_filter COMMAND STP2GLB --stp "${CMAKE_CURRENT_SOURCE_DIR}/files/as1-oc-214.stp" --glb ${CMAKE_CURRENT_SOURCE_DIR}/temp/as1-oc-214-filtered.glb @@ -54,4 +78,4 @@ if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/temp/really_large.stp") --filter-names-file-exclude=${CMAKE_CURRENT_SOURCE_DIR}/temp/skip-these-nodes.txt WORKING_DIRECTORY "${CMAKE_INSTALL_PREFIX}/bin" ) -endif () \ No newline at end of file +endif ()