Add configurable GLB node naming
This commit is contained in:
parent
70ccc02cc4
commit
4d0af82aab
@ -83,6 +83,7 @@ set(SOURCES
|
|||||||
src/cadit/occt/debug.cpp
|
src/cadit/occt/debug.cpp
|
||||||
src/cadit/occt/gltf_writer.cpp
|
src/cadit/occt/gltf_writer.cpp
|
||||||
src/cadit/occt/convert.cpp
|
src/cadit/occt/convert.cpp
|
||||||
|
src/cadit/occt/node_naming.cpp
|
||||||
src/cadit/occt/step_helpers.cpp
|
src/cadit/occt/step_helpers.cpp
|
||||||
src/cadit/occt/bsplinesurf.cpp
|
src/cadit/occt/bsplinesurf.cpp
|
||||||
src/cadit/occt/helpers.cpp
|
src/cadit/occt/helpers.cpp
|
||||||
@ -98,6 +99,7 @@ set(HEADERS
|
|||||||
src/geom/Color.h
|
src/geom/Color.h
|
||||||
src/cadit/occt/step_tree.h
|
src/cadit/occt/step_tree.h
|
||||||
src/cadit/occt/convert.h
|
src/cadit/occt/convert.h
|
||||||
|
src/cadit/occt/node_naming.h
|
||||||
src/cadit/occt/debug.h
|
src/cadit/occt/debug.h
|
||||||
src/cadit/occt/step_helpers.h
|
src/cadit/occt/step_helpers.h
|
||||||
src/cadit/occt/gltf_writer.h
|
src/cadit/occt/gltf_writer.h
|
||||||
@ -142,4 +144,3 @@ if (BUILD_TESTING)
|
|||||||
enable_testing()
|
enable_testing()
|
||||||
include(tests/tests.cmake)
|
include(tests/tests.cmake)
|
||||||
endif (BUILD_TESTING)
|
endif (BUILD_TESTING)
|
||||||
|
|
||||||
|
|||||||
4
desktop/tmp-render.ts
Normal file
4
desktop/tmp-render.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
|
import App from "./src/App";
|
||||||
|
|
||||||
|
console.log(renderToStaticMarkup(<App />));
|
||||||
@ -29,6 +29,7 @@
|
|||||||
|
|
||||||
#include "custom_progress.h"
|
#include "custom_progress.h"
|
||||||
#include "geometry_iterator.h"
|
#include "geometry_iterator.h"
|
||||||
|
#include "node_naming.h"
|
||||||
#include "step_helpers.h"
|
#include "step_helpers.h"
|
||||||
#include "step_tree.h"
|
#include "step_tree.h"
|
||||||
#include "../../config_structs.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;
|
std::cout << "Reading STEP file: " << config.stpFile << std::endl;
|
||||||
if (reader.ReadFile(config.stpFile.string().c_str()) != IFSelect_RetDone)
|
if (reader.ReadFile(config.stpFile.string().c_str()) != IFSelect_RetDone)
|
||||||
throw std::runtime_error("Error reading STEP file");
|
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 stop = std::chrono::high_resolution_clock::now();
|
||||||
auto duration = std::chrono::duration<double>(stop - start).count();
|
auto duration = std::chrono::duration<double>(stop - start).count();
|
||||||
|
|
||||||
@ -334,6 +338,7 @@ void convert_stp_to_glb(const GlobalConfig& config)
|
|||||||
throw std::runtime_error("Error writing GLB file");
|
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);
|
apply_world_translation_to_glb(config.glbFile, root_translation);
|
||||||
stop = std::chrono::high_resolution_clock::now();
|
stop = std::chrono::high_resolution_clock::now();
|
||||||
duration = std::chrono::duration<double>(stop - start).count();
|
duration = std::chrono::duration<double>(stop - start).count();
|
||||||
@ -354,4 +359,3 @@ void convert_stp_to_glb(const GlobalConfig& config)
|
|||||||
log_file << "]\n";
|
log_file << "]\n";
|
||||||
log_file.close();
|
log_file.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -106,7 +106,7 @@ void debug_stp_to_glb(const GlobalConfig &config) {
|
|||||||
std::cout << "Number of entities: " << num_entities << "\n";
|
std::cout << "Number of entities: " << num_entities << "\n";
|
||||||
|
|
||||||
// Extract hierarchy
|
// Extract hierarchy
|
||||||
auto roots = ExtractProductHierarchy(model, theGraph);
|
auto roots = ExtractProductHierarchy(model, theGraph, config.nodeNameMode);
|
||||||
add_geometries_to_nodes(roots, theGraph);
|
add_geometries_to_nodes(roots, theGraph);
|
||||||
|
|
||||||
auto step_store = StepStore(roots);
|
auto step_store = StepStore(roots);
|
||||||
|
|||||||
306
src/cadit/occt/node_naming.cpp
Normal file
306
src/cadit/occt/node_naming.cpp
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
#include "node_naming.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <cstring>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <StepBasic_ProductDefinitionFormation.hxx>
|
||||||
|
#include <TCollection_HAsciiString.hxx>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
#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<char>(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<char>& 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<char> 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<std::uint32_t>(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<std::uint32_t>(padded_json_length);
|
||||||
|
const char* json_length_bytes = reinterpret_cast<const char*>(&new_json_length);
|
||||||
|
output.insert(output.end(), json_length_bytes, json_length_bytes + sizeof(std::uint32_t));
|
||||||
|
|
||||||
|
const char* chunk_type_bytes = reinterpret_cast<const char*>(&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<std::streamsize>(output.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
int default_scene_index(const nlohmann::json& document)
|
||||||
|
{
|
||||||
|
if (document.contains("scene") && document["scene"].is_number_integer()) {
|
||||||
|
return document["scene"].get<int>();
|
||||||
|
}
|
||||||
|
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<int>(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<std::string>() != 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<int>(),
|
||||||
|
*product_node.children[i]) || changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool apply_product_name_tree_to_gltf(nlohmann::json& document,
|
||||||
|
const std::vector<std::unique_ptr<ProductNode>>& 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<int>(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<int>(),
|
||||||
|
*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<char> data(static_cast<std::size_t>(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/cadit/occt/node_naming.h
Normal file
32
src/cadit/occt/node_naming.h
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
#ifndef STP2GLB_NODE_NAMING_H
|
||||||
|
#define STP2GLB_NODE_NAMING_H
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <Interface_Graph.hxx>
|
||||||
|
#include <Interface_InterfaceModel.hxx>
|
||||||
|
#include <Standard_Handle.hxx>
|
||||||
|
#include <StepBasic_Product.hxx>
|
||||||
|
#include <StepBasic_ProductDefinition.hxx>
|
||||||
|
#include <StepRepr_NextAssemblyUsageOccurrence.hxx>
|
||||||
|
|
||||||
|
#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
|
||||||
@ -3,6 +3,7 @@
|
|||||||
#include <Interface_EntityIterator.hxx>
|
#include <Interface_EntityIterator.hxx>
|
||||||
|
|
||||||
// STEP entity classes
|
// STEP entity classes
|
||||||
|
#include <algorithm>
|
||||||
#include <StepBasic_Product.hxx>
|
#include <StepBasic_Product.hxx>
|
||||||
#include <StepBasic_ProductDefinition.hxx>
|
#include <StepBasic_ProductDefinition.hxx>
|
||||||
#include <StepBasic_ProductDefinitionFormation.hxx>
|
#include <StepBasic_ProductDefinitionFormation.hxx>
|
||||||
@ -30,6 +31,7 @@
|
|||||||
#include <StepGeom_CartesianPoint.hxx>
|
#include <StepGeom_CartesianPoint.hxx>
|
||||||
#include <StepGeom_Direction.hxx>
|
#include <StepGeom_Direction.hxx>
|
||||||
|
|
||||||
|
#include "node_naming.h"
|
||||||
#include "step_helpers.h"
|
#include "step_helpers.h"
|
||||||
|
|
||||||
|
|
||||||
@ -343,7 +345,8 @@ BuildAssemblyLinksWithTransformation(const Handle(Interface_InterfaceModel) &mod
|
|||||||
int ProductNode::instanceCounter = 0;
|
int ProductNode::instanceCounter = 0;
|
||||||
// Main function: extracts top-level ProductNode trees with transformations
|
// Main function: extracts top-level ProductNode trees with transformations
|
||||||
std::vector<std::unique_ptr<ProductNode> > ExtractProductHierarchy(const Handle(Interface_InterfaceModel) &model,
|
std::vector<std::unique_ptr<ProductNode> > 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
|
// 1) Build the map of parent->children relationships
|
||||||
// to be replaced by this
|
// to be replaced by this
|
||||||
const auto parentToChildrenWTransforms = BuildAssemblyLinksWithTransformation(model, theGraph);
|
const auto parentToChildrenWTransforms = BuildAssemblyLinksWithTransformation(model, theGraph);
|
||||||
@ -362,6 +365,7 @@ std::vector<std::unique_ptr<ProductNode> > ExtractProductHierarchy(const Handle(
|
|||||||
for (auto productIndexMap = BuildProductIndexMap(model); auto &val: productIndexMap | std::views::values) {
|
for (auto productIndexMap = BuildProductIndexMap(model); auto &val: productIndexMap | std::views::values) {
|
||||||
allProducts.push_back(val);
|
allProducts.push_back(val);
|
||||||
}
|
}
|
||||||
|
std::sort(allProducts.begin(), allProducts.end());
|
||||||
|
|
||||||
// 4) For each product, if it’s NOT in allChildren => it’s a root
|
// 4) For each product, if it’s NOT in allChildren => it’s a root
|
||||||
std::vector<std::unique_ptr<ProductNode> > roots;
|
std::vector<std::unique_ptr<ProductNode> > roots;
|
||||||
@ -369,7 +373,8 @@ std::vector<std::unique_ptr<ProductNode> > ExtractProductHierarchy(const Handle(
|
|||||||
if (!allChildren.contains(idx)) {
|
if (!allChildren.contains(idx)) {
|
||||||
// This is a root product, start with the identity transformation
|
// This is a root product, start with the identity transformation
|
||||||
roots.push_back(
|
roots.push_back(
|
||||||
BuildProductNodeWithTransform(idx, parentToChildrenWTransforms, model, theGraph, gp_Trsf()));
|
BuildProductNodeWithTransform(idx, parentToChildrenWTransforms, model, theGraph, gp_Trsf(), nullptr,
|
||||||
|
node_name_mode));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return roots;
|
return roots;
|
||||||
@ -447,7 +452,9 @@ static std::unique_ptr<ProductNode> BuildProductNodeWithTransform(
|
|||||||
const Handle(Interface_InterfaceModel) &model,
|
const Handle(Interface_InterfaceModel) &model,
|
||||||
const Interface_Graph &theGraph,
|
const Interface_Graph &theGraph,
|
||||||
const gp_Trsf &parentTransform,
|
const gp_Trsf &parentTransform,
|
||||||
ProductNode *parent) {
|
ProductNode *parent,
|
||||||
|
NodeNameMode node_name_mode,
|
||||||
|
const ParentChildRelationship* relationship) {
|
||||||
auto node = std::make_unique<ProductNode>();
|
auto node = std::make_unique<ProductNode>();
|
||||||
node->entityIndex = productIndex;
|
node->entityIndex = productIndex;
|
||||||
node->parent = parent;
|
node->parent = parent;
|
||||||
@ -455,10 +462,29 @@ static std::unique_ptr<ProductNode> BuildProductNodeWithTransform(
|
|||||||
const Handle(Standard_Transient) ent = model->Value(productIndex);
|
const Handle(Standard_Transient) ent = model->Value(productIndex);
|
||||||
const auto product = Handle(StepBasic_Product)::DownCast(ent);
|
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();
|
node->name = product->Name()->ToCString();
|
||||||
} else {
|
} else {
|
||||||
node->name = "(unnamed product)";
|
if (node->name.empty()) {
|
||||||
|
node->name = "(unnamed product)";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combine parent transformation with local transformation
|
// Combine parent transformation with local transformation
|
||||||
@ -477,7 +503,7 @@ static std::unique_ptr<ProductNode> BuildProductNodeWithTransform(
|
|||||||
// Build the child node
|
// Build the child node
|
||||||
node->children.push_back(
|
node->children.push_back(
|
||||||
BuildProductNodeWithTransform(childRel.childIndex, parentToChildrenWTransforms, model, theGraph,
|
BuildProductNodeWithTransform(childRel.childIndex, parentToChildrenWTransforms, model, theGraph,
|
||||||
childAbsoluteTransform, node.get()));
|
childAbsoluteTransform, node.get(), node_name_mode, &childRel));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,8 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include "../../config_structs.h"
|
||||||
|
|
||||||
struct ProcessResult {
|
struct ProcessResult {
|
||||||
mutable bool added_to_model;
|
mutable bool added_to_model;
|
||||||
mutable std::string skip_reason;
|
mutable std::string skip_reason;
|
||||||
@ -70,7 +72,8 @@ struct ParentChildRelationship {
|
|||||||
};
|
};
|
||||||
|
|
||||||
std::vector<std::unique_ptr<ProductNode> > ExtractProductHierarchy(const Handle(Interface_InterfaceModel) &model,
|
std::vector<std::unique_ptr<ProductNode> > 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<std::unique_ptr<ProductNode> > &roots);
|
std::string ExportHierarchyToJson(const std::vector<std::unique_ptr<ProductNode> > &roots);
|
||||||
|
|
||||||
@ -86,7 +89,9 @@ static std::unique_ptr<ProductNode> BuildProductNodeWithTransform(
|
|||||||
const Handle(Interface_InterfaceModel)& model,
|
const Handle(Interface_InterfaceModel)& model,
|
||||||
const Interface_Graph& theGraph,
|
const Interface_Graph& theGraph,
|
||||||
const gp_Trsf& parentTransform = gp_Trsf(),
|
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<ProductNode> BuildProductNodeWithTransformIterative(
|
static std::unique_ptr<ProductNode> BuildProductNodeWithTransformIterative(
|
||||||
int rootIndex,
|
int rootIndex,
|
||||||
|
|||||||
@ -9,6 +9,14 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
enum class NodeNameMode {
|
||||||
|
Source,
|
||||||
|
Instance,
|
||||||
|
Title,
|
||||||
|
TitleVersion,
|
||||||
|
Combined
|
||||||
|
};
|
||||||
|
|
||||||
struct BuildConfig {
|
struct BuildConfig {
|
||||||
bool build_bspline_surf = false;
|
bool build_bspline_surf = false;
|
||||||
};
|
};
|
||||||
@ -40,6 +48,7 @@ struct GlobalConfig {
|
|||||||
|
|
||||||
std::vector<std::string> filter_names_include;
|
std::vector<std::string> filter_names_include;
|
||||||
std::vector<std::string> filter_names_exclude;
|
std::vector<std::string> filter_names_exclude;
|
||||||
|
NodeNameMode nodeNameMode = NodeNameMode::Source;
|
||||||
|
|
||||||
BuildConfig buildConfig;
|
BuildConfig buildConfig;
|
||||||
ServerConfig serverConfig;
|
ServerConfig serverConfig;
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
#include "config_structs.h"
|
#include "config_structs.h"
|
||||||
#include "cadit/occt/helpers.h"
|
#include "cadit/occt/helpers.h"
|
||||||
|
#include "cadit/occt/node_naming.h"
|
||||||
#include "CLI/App.hpp"
|
#include "CLI/App.hpp"
|
||||||
#include "http_downloader.h"
|
#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<std::string>();
|
const auto compressed_glb_value = app.get_option("--compressed-glb")->as<std::string>();
|
||||||
const auto gltfpack_path_value = app.get_option("--gltfpack-path")->as<std::string>();
|
const auto gltfpack_path_value = app.get_option("--gltfpack-path")->as<std::string>();
|
||||||
const auto gltfpack_args_value = app.get_option("--gltfpack-args")->as<std::string>();
|
const auto gltfpack_args_value = app.get_option("--gltfpack-args")->as<std::string>();
|
||||||
|
const auto node_name_mode_value = app.get_option("--node-name-mode")->as<std::string>();
|
||||||
|
|
||||||
// Process include and exclude filter names
|
// Process include and exclude filter names
|
||||||
const auto filter_names_include = process_filter_names(filter_names_include_input, filter_names_file_include);
|
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<int>(),
|
.tessellation_timout = app.get_option("--tessellation-timeout")->as<int>(),
|
||||||
.filter_names_include = filter_names_include,
|
.filter_names_include = filter_names_include,
|
||||||
.filter_names_exclude = filter_names_exclude,
|
.filter_names_exclude = filter_names_exclude,
|
||||||
|
.nodeNameMode = parse_node_name_mode(node_name_mode_value),
|
||||||
.buildConfig = {},
|
.buildConfig = {},
|
||||||
.serverConfig = {
|
.serverConfig = {
|
||||||
.enable_server = app.get_option("--server")->as<bool>(),
|
.enable_server = app.get_option("--server")->as<bool>(),
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
#include "third_party/httplib.h"
|
#include "third_party/httplib.h"
|
||||||
#include "cadit/occt/convert.h"
|
#include "cadit/occt/convert.h"
|
||||||
#include "cadit/occt/debug.h"
|
#include "cadit/occt/debug.h"
|
||||||
|
#include "cadit/occt/node_naming.h"
|
||||||
#include "http_downloader.h"
|
#include "http_downloader.h"
|
||||||
#include "compression_utils.h"
|
#include "compression_utils.h"
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
@ -95,6 +96,16 @@ void start_http_server(const GlobalConfig& base_config) {
|
|||||||
if (req.form.has_field("maxGeometryNum")) {
|
if (req.form.has_field("maxGeometryNum")) {
|
||||||
config.max_geometry_num = std::stoi(req.form.get_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")) {
|
if (req.form.has_field("compressGlb")) {
|
||||||
config.compress_glb = req.form.get_field("compressGlb") == "true";
|
config.compress_glb = req.form.get_field("compressGlb") == "true";
|
||||||
|
|||||||
22
src/main.cpp
22
src/main.cpp
@ -13,6 +13,7 @@
|
|||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include "cadit/occt/debug.h"
|
#include "cadit/occt/debug.h"
|
||||||
#include "cadit/occt/convert.h"
|
#include "cadit/occt/convert.h"
|
||||||
|
#include "cadit/occt/node_naming.h"
|
||||||
#include "cadit/occt/bsplinesurf.h"
|
#include "cadit/occt/bsplinesurf.h"
|
||||||
#include "cadit/occt/helpers.h"
|
#include "cadit/occt/helpers.h"
|
||||||
#include "config_utils.h"
|
#include "config_utils.h"
|
||||||
@ -189,6 +190,25 @@ void print_status(const GlobalConfig& config) {
|
|||||||
std::cout << "Solid Only: " << config.solidOnly << "\n";
|
std::cout << "Solid Only: " << config.solidOnly << "\n";
|
||||||
std::cout << "Max Geometry Num: " << config.max_geometry_num << "\n";
|
std::cout << "Max Geometry Num: " << config.max_geometry_num << "\n";
|
||||||
std::cout << "Tessellation Timeout: " << config.tessellation_timout << "\n\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";
|
std::cout << "Compression: " << (config.compress_glb ? "enabled" : "disabled") << "\n";
|
||||||
if (config.compress_glb) {
|
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-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("--filter-names-file-exclude", "Exclude Filter name file")->default_val("");
|
||||||
app.add_option("--tessellation-timeout", "Tessellation timeout")->default_val(30);
|
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_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("--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");
|
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.solidOnly = false;
|
||||||
config.max_geometry_num = 0;
|
config.max_geometry_num = 0;
|
||||||
config.tessellation_timout = app.get_option("--tessellation-timeout")->as<int>();
|
config.tessellation_timout = app.get_option("--tessellation-timeout")->as<int>();
|
||||||
|
config.nodeNameMode = parse_node_name_mode(app.get_option("--node-name-mode")->as<std::string>());
|
||||||
|
|
||||||
config.serverConfig.enable_server = true;
|
config.serverConfig.enable_server = true;
|
||||||
config.serverConfig.port = app.get_option("--port")->as<int>();
|
config.serverConfig.port = app.get_option("--port")->as<int>();
|
||||||
|
|||||||
31
tests/check_node_names.ps1
Normal file
31
tests/check_node_names.ps1
Normal file
@ -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 ', ')"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -35,6 +35,30 @@ add_test(NAME debug_as1_mini COMMAND STP2GLB
|
|||||||
WORKING_DIRECTORY "${CMAKE_INSTALL_PREFIX}/bin"
|
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 $<TARGET_FILE:STP2GLB>
|
||||||
|
-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 $<TARGET_FILE:STP2GLB>
|
||||||
|
-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
|
add_test(NAME debug_as1_filter COMMAND STP2GLB
|
||||||
--stp "${CMAKE_CURRENT_SOURCE_DIR}/files/as1-oc-214.stp"
|
--stp "${CMAKE_CURRENT_SOURCE_DIR}/files/as1-oc-214.stp"
|
||||||
--glb ${CMAKE_CURRENT_SOURCE_DIR}/temp/as1-oc-214-filtered.glb
|
--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
|
--filter-names-file-exclude=${CMAKE_CURRENT_SOURCE_DIR}/temp/skip-these-nodes.txt
|
||||||
WORKING_DIRECTORY "${CMAKE_INSTALL_PREFIX}/bin"
|
WORKING_DIRECTORY "${CMAKE_INSTALL_PREFIX}/bin"
|
||||||
)
|
)
|
||||||
endif ()
|
endif ()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user