feat: 添加 HTTP 服务器模式和低精度默认配置

新增功能:
- 添加 HTTP 服务器模式,支持通过 API 转换 STEP 文件
- 实现文件上传和转换接口 POST /convert
- 添加健康检查接口 GET /health
- 支持服务器端输出目录配置 (默认 ./output)
- 调整默认精度为 Low 级别以提升转换速度

技术实现:
- 集成 cpp-httplib 作为 HTTP 服务器
- 扩展 ServerConfig 配置结构
- 实现临时文件自动清理机制
- 返回 JSON 格式响应,包含输出文件路径

使用方式:
- CLI 模式: STP2GLB.exe --stp input.stp --glb output.glb
- 服务器模式: STP2GLB.exe --server --port 8080

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
sladro 2025-10-11 17:04:47 +08:00
commit bc9165803b
7 changed files with 12517 additions and 0 deletions

137
CMakeLists.txt Normal file
View File

@ -0,0 +1,137 @@
cmake_minimum_required(VERSION 3.20)
project(stp2glb LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
OPTION(CONDA_LOCAL_DEV "Install into activate local development conda env." OFF)
if (CONDA_LOCAL_DEV)
message(STATUS "Building for local development conda env.")
set(EXE_BIN_DIR ${CMAKE_INSTALL_PREFIX}/bin)
endif ()
include(cmake/pre_checks.cmake)
# Create a empty list to hold all the linked libs
set(ADA_CPP_LINK_LIBS)
# Add dependencies
include(cmake/deps_occ.cmake)
# Add an option to enable static builds
OPTION(BUILD_STATIC "Build STP2GLB as a static executable." OFF)
if (BUILD_STATIC)
message(STATUS "Building STP2GLB as a static executable.")
# Force static linking
set(BUILD_SHARED_LIBS OFF) # Disable shared libraries
set(CMAKE_FIND_LIBRARY_SUFFIXES ".lib" ".a") # Prefer static libraries
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static")
# For MSVC, ensure the static runtime is used
if (MSVC)
foreach (flag_var CMAKE_CXX_FLAGS CMAKE_CXX_FLAGS_DEBUG CMAKE_CXX_FLAGS_RELEASE
CMAKE_CXX_FLAGS_MINSIZEREL CMAKE_CXX_FLAGS_RELWITHDEBINFO)
set(${flag_var} "${${flag_var}} /MT")
endforeach ()
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MT")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MT")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MTd")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /MT")
set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} /MT")
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} /MTd")
# Force dependencies to link statically
set(CMAKE_FIND_PACKAGE_PREFER_CONFIG TRUE) # Prefer CMake config files
foreach (lib ${ADA_CPP_LINK_LIBS})
if (TARGET ${lib})
set_target_properties(${lib} PROPERTIES
BUILD_WITH_INSTALL_RPATH TRUE
LINK_SEARCH_START_STATIC TRUE
LINK_SEARCH_END_STATIC TRUE
)
endif ()
endforeach ()
endif ()
else ()
include(cmake/deps_ifc.cmake)
include(cmake/deps_stepcode.cmake)
include(cmake/deps_cgal.cmake)
endif ()
if (MSVC)
add_definitions(-DWIN32_LEAN_AND_MEAN)
add_definitions(-DNOMINMAX)
endif ()
# Install the C++ executable
set(SOURCES
src/main.cpp
src/config_utils.cpp
src/http_server.cpp
src/geom/Color.cpp
src/cadit/occt/step_tree.cpp
src/cadit/occt/debug.cpp
src/cadit/occt/gltf_writer.cpp
src/cadit/occt/convert.cpp
src/cadit/occt/step_helpers.cpp
src/cadit/occt/bsplinesurf.cpp
src/cadit/occt/helpers.cpp
src/cadit/occt/step_writer.cpp
src/cadit/occt/custom_progress.cpp
)
set(HEADERS
src/config_utils.h
src/config_structs.h
src/http_server.h
src/geom/Color.h
src/cadit/occt/step_tree.h
src/cadit/occt/convert.h
src/cadit/occt/debug.h
src/cadit/occt/step_helpers.h
src/cadit/occt/gltf_writer.h
src/cadit/occt/bsplinesurf.h
src/cadit/occt/helpers.h
src/cadit/occt/step_writer.h
src/cadit/occt/geometry_iterator.h
src/cadit/occt/custom_progress.h
)
add_executable(STP2GLB ${SOURCES} ${HEADERS})
if (NOT BUILD_STATIC)
set_target_properties(STP2GLB PROPERTIES
INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/bin")
endif ()
message(STATUS "CLI11_DIR: ${CLI11_DIR}")
if (BUILD_STATIC)
target_include_directories(STP2GLB PRIVATE ${CLI11_DIR})
if (UNIX)
message(STATUS "Setting static compilation rules for unix")
target_link_libraries(STP2GLB PRIVATE ${ADA_CPP_LINK_LIBS} pthread -static)
else ()
target_link_libraries(STP2GLB ${ADA_CPP_LINK_LIBS} -static)
set_property(TARGET STP2GLB PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
endif ()
else ()
target_link_libraries(STP2GLB ${ADA_CPP_LINK_LIBS})
endif ()
# install to executable into the bin dir
message(STATUS "Installing executable to ${EXE_BIN_DIR}")
install(TARGETS STP2GLB DESTINATION ${EXE_BIN_DIR})
# Include the tests directory
OPTION(BUILD_TESTING "Build the testing tree." ON)
if (BUILD_TESTING)
message(STATUS "Building the testing tree.")
enable_testing()
include(tests/tests.cmake)
endif (BUILD_TESTING)

49
src/config_structs.h Normal file
View File

@ -0,0 +1,49 @@
//
// Created by ofskrand on 07.01.2025.
//
#ifndef CONFIG_STRUCTS_H
#define CONFIG_STRUCTS_H
#include <vector>
#include <filesystem>
struct BuildConfig {
bool build_bspline_surf;
};
struct ServerConfig {
bool enable_server = false;
int port = 8080;
std::string host = "0.0.0.0";
size_t max_file_size_mb = 500;
std::string temp_dir = "./temp";
std::string output_dir = "./output";
};
struct GlobalConfig {
std::filesystem::path stpFile;
std::filesystem::path glbFile;
// Conversion parameters
bool debug_mode;
double linearDeflection;
double angularDeflection;
bool relativeDeflection;
// Debug parameters
bool solidOnly;
int max_geometry_num;
int tessellation_timout;
std::vector<std::string> filter_names_include;
std::vector<std::string> filter_names_exclude;
BuildConfig buildConfig;
ServerConfig serverConfig;
};
#endif //CONFIG_STRUCTS_H

131
src/config_utils.cpp Normal file
View File

@ -0,0 +1,131 @@
//
// Created by ofskrand on 13.01.2025.
//
#include "config_utils.h"
#include <fstream>
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>
#include "config_structs.h"
#include "cadit/occt/helpers.h"
#include "CLI/App.hpp"
// Helper function to process filter names from input or file
std::vector<std::string> process_filter_names(const std::string& input, const std::string& file_name)
{
std::vector<std::string> filter_names;
// Process input string
if (!input.empty())
{
std::string stripped_input = strip_quotes(input);
auto names = split(stripped_input, ',');
filter_names.insert(filter_names.end(), names.begin(), names.end());
}
// Process file
if (!file_name.empty())
{
if (std::ifstream file(file_name); file.is_open())
{
std::string line;
while (std::getline(file, line))
{
if (!line.empty())
{
filter_names.push_back(line);
}
}
file.close();
}
else
{
throw std::runtime_error("Error: Could not open file: " + file_name);
}
}
return filter_names;
}
// Helper function to check if a string ends with a specific suffix (case insensitive)
bool endsWithCaseInsensitive(const std::string& str, const std::string& suffix) {
if (str.size() < suffix.size()) {
return false;
}
auto strIt = str.end() - suffix.size();
auto suffixIt = suffix.begin();
while (suffixIt != suffix.end()) {
if (tolower(*strIt++) != tolower(*suffixIt++)) {
return false;
}
}
return true;
}
// Main processing function
GlobalConfig process_parameters(CLI::App& app)
{
const auto filter_names_include_input = app.get_option("--filter-names-include")->as<std::string>();
const auto filter_names_file_include = app.get_option("--filter-names-file-include")->as<std::string>();
const auto filter_names_exclude_input = app.get_option("--filter-names-exclude")->as<std::string>();
const auto filter_names_file_exclude = app.get_option("--filter-names-file-exclude")->as<std::string>();
// 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_exclude = process_filter_names(filter_names_exclude_input, filter_names_file_exclude);
const std::string stpFilename = app.get_option("--stp")->results()[0];
const std::string glbFilename = app.get_option("--glb")->results()[0];
// Validate extensions
const bool isStpValid = endsWithCaseInsensitive(stpFilename, ".stp") || endsWithCaseInsensitive(stpFilename, ".step");
const bool isGlbValid = endsWithCaseInsensitive(glbFilename, ".glb");
auto stpFilePath = std::filesystem::path(stpFilename);
auto glbFilePath = std::filesystem::path(glbFilename);
// check if file paths exists
if (!exists(stpFilePath)) {
throw std::invalid_argument("Invalid --stp filename \"" + stpFilename + "\". File does not exist.");
}
if (exists(glbFilePath)) {
std::cout << "Warning: --glb filename \"" << glbFilename << "\" already exists and will be overwritten.\n";
}
if (!isStpValid) {
throw std::invalid_argument("Invalid --stp filename. It must end with .stp or .step.");
}
if (!isGlbValid) {
throw std::invalid_argument("Invalid --glb filename. It must end with .glb.");
}
// Create configuration
return {
.stpFile = stpFilename,
.glbFile = glbFilename,
.debug_mode = app.get_option("--debug")->as<bool>(),
.linearDeflection = app.get_option("--lin-defl")->as<double>(),
.angularDeflection = app.get_option("--ang-defl")->as<double>(),
.relativeDeflection = app.get_option("--rel-defl")->as<bool>(),
.solidOnly = app.get_option("--solid-only")->as<bool>(),
.max_geometry_num = app.get_option("--max-geometry-num")->as<int>(),
.tessellation_timout = app.get_option("--tessellation-timeout")->as<int>(),
.filter_names_include = filter_names_include,
.filter_names_exclude = filter_names_exclude,
.buildConfig = {
.build_bspline_surf = false
},
.serverConfig = {
.enable_server = app.get_option("--server")->as<bool>(),
.port = app.get_option("--port")->as<int>(),
.host = app.get_option("--host")->as<std::string>(),
.max_file_size_mb = app.get_option("--max-file-size")->as<size_t>(),
.temp_dir = "./temp"
}
};
}

137
src/http_server.cpp Normal file
View File

@ -0,0 +1,137 @@
#include "http_server.h"
#include "third_party/httplib.h"
#include "cadit/occt/convert.h"
#include "cadit/occt/debug.h"
#include <iostream>
#include <filesystem>
#include <fstream>
#include <chrono>
#include <random>
#include <sstream>
#include <iomanip>
namespace fs = std::filesystem;
std::string generate_unique_filename(const std::string& extension) {
auto now = std::chrono::system_clock::now();
auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dis(1000, 9999);
std::ostringstream oss;
oss << timestamp << "_" << dis(gen) << extension;
return oss.str();
}
void start_http_server(const GlobalConfig& base_config) {
httplib::Server svr;
fs::create_directories(base_config.serverConfig.temp_dir);
fs::create_directories(base_config.serverConfig.output_dir);
std::cout << "STP2GLB HTTP Server" << "\n";
std::cout << "===================" << "\n";
std::cout << "Host: " << base_config.serverConfig.host << "\n";
std::cout << "Port: " << base_config.serverConfig.port << "\n";
std::cout << "Temp Dir: " << base_config.serverConfig.temp_dir << "\n";
std::cout << "Output Dir: " << base_config.serverConfig.output_dir << "\n";
std::cout << "Max File Size: " << base_config.serverConfig.max_file_size_mb << " MB" << "\n\n";
svr.set_payload_max_length(base_config.serverConfig.max_file_size_mb * 1024 * 1024);
svr.Get("/health", [](const httplib::Request& req, httplib::Response& res) {
res.set_content("{\"status\":\"ok\",\"service\":\"STP2GLB\"}", "application/json");
});
svr.Post("/convert", [&base_config](const httplib::Request& req, httplib::Response& res) {
std::cout << "Received conversion request" << "\n";
if (!req.is_multipart_form_data() || !req.form.has_file("file")) {
res.status = 400;
res.set_content("{\"error\":\"No file uploaded. Use 'file' field.\"}", "application/json");
return;
}
const auto& file = req.form.get_file("file", 0);
GlobalConfig config = base_config;
if (req.form.has_field("linearDeflection")) {
config.linearDeflection = std::stod(req.form.get_field("linearDeflection"));
}
if (req.form.has_field("angularDeflection")) {
config.angularDeflection = std::stod(req.form.get_field("angularDeflection"));
}
if (req.form.has_field("relativeDeflection")) {
config.relativeDeflection = req.form.get_field("relativeDeflection") == "true";
}
if (req.form.has_field("debug")) {
config.debug_mode = req.form.get_field("debug") == "true";
}
if (req.form.has_field("solidOnly")) {
config.solidOnly = req.form.get_field("solidOnly") == "true";
}
if (req.form.has_field("maxGeometryNum")) {
config.max_geometry_num = std::stoi(req.form.get_field("maxGeometryNum"));
}
std::string base_filename = generate_unique_filename("");
std::string stp_filename = base_filename + ".stp";
std::string glb_filename = base_filename + ".glb";
fs::path stp_path = fs::path(config.serverConfig.temp_dir) / stp_filename;
fs::path glb_path = fs::path(config.serverConfig.output_dir) / glb_filename;
try {
std::ofstream ofs(stp_path, std::ios::binary);
ofs.write(file.content.c_str(), file.content.size());
ofs.close();
config.stpFile = stp_path;
config.glbFile = glb_path;
std::cout << "Converting: " << stp_filename << " -> " << glb_filename << "\n";
std::cout << " Linear Deflection: " << config.linearDeflection << "\n";
std::cout << " Angular Deflection: " << config.angularDeflection << "\n";
std::cout << " Debug Mode: " << config.debug_mode << "\n";
const auto start = std::chrono::high_resolution_clock::now();
if (config.debug_mode) {
debug_stp_to_glb(config);
} else {
convert_stp_to_glb(config);
}
const auto stop = std::chrono::high_resolution_clock::now();
const auto duration = std::chrono::duration_cast<std::chrono::microseconds>(stop - start);
const double seconds = static_cast<double>(duration.count()) / 1e6;
std::cout << "Conversion completed in " << std::fixed << std::setprecision(2) << seconds << " seconds" << "\n";
std::cout << "Output saved: " << glb_path << "\n";
fs::remove(stp_path);
std::string response = "{\"success\":true,\"output_file\":\"" + glb_filename + "\",\"output_path\":\"" + glb_path.string() + "\",\"conversion_time\":" + std::to_string(seconds) + "}";
res.set_content(response, "application/json");
} catch (const std::exception& ex) {
std::cerr << "Error: " << ex.what() << "\n";
if (fs::exists(stp_path)) fs::remove(stp_path);
if (fs::exists(glb_path)) fs::remove(glb_path);
res.status = 500;
std::string error_msg = "{\"success\":false,\"error\":\"" + std::string(ex.what()) + "\"}";
res.set_content(error_msg, "application/json");
}
});
std::cout << "Server starting..." << "\n";
std::cout << "Listening on http://" << base_config.serverConfig.host << ":" << base_config.serverConfig.port << "\n";
std::cout << "Press Ctrl+C to stop" << "\n\n";
svr.listen(base_config.serverConfig.host, base_config.serverConfig.port);
}

8
src/http_server.h Normal file
View File

@ -0,0 +1,8 @@
#ifndef HTTP_SERVER_H
#define HTTP_SERVER_H
#include "config_structs.h"
void start_http_server(const GlobalConfig& config);
#endif //HTTP_SERVER_H

144
src/main.cpp Normal file
View File

@ -0,0 +1,144 @@
// Check if the platform is Unix-based
#if defined(__unix__) || defined(__unix)
#include <cstdint>
#endif // Unix platform check
#include <filesystem>
#include "CLI/CLI.hpp"
#include "config_structs.h"
#include <chrono>
#include "cadit/occt/debug.h"
#include "cadit/occt/convert.h"
#include "cadit/occt/bsplinesurf.h"
#include "cadit/occt/helpers.h"
#include "config_utils.h"
#include "http_server.h"
void print_status(const GlobalConfig& config) {
std::cout << "STP2GLB Converter" << "\n";
std::cout << "STP File: " << config.stpFile << "\n";
std::cout << "GLB File: " << config.glbFile << "\n\n";
std::cout << "Tessellation Parameters: " << "\n";
std::cout << "Linear Deflection: " << config.linearDeflection << "\n";
std::cout << "Angular Deflection: " << config.angularDeflection << "\n";
std::cout << "Relative Deflection: " << config.relativeDeflection << "\n\n";
std::cout << "Debug Parameters: " << "\n";
std::cout << "Debug Mode: " << config.debug_mode << "\n";
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";
// Debug output
if (!config.filter_names_include.empty())
{
std::cout << "Included Filter Names:" << std::endl;
for (const auto& name : config.filter_names_include)
{
std::cout << name << std::endl;
}
}
if (!config.filter_names_exclude.empty())
{
std::cout << "Excluded Filter Names:" << std::endl;
for (const auto& name : config.filter_names_exclude)
{
std::cout << name << std::endl;
}
}
}
int main(int argc, char* argv[])
{
CLI::App app{"STEP to GLB converter"};
app.add_flag("--server", "Run as HTTP server");
app.add_option("--port", "Server port")->default_val(8080);
app.add_option("--host", "Server host")->default_val("0.0.0.0");
app.add_option("--max-file-size", "Maximum file size in MB")->default_val(500);
app.add_option("--stp", "STEP filepath");
app.add_option("--glb", "GLB filepath");
app.add_option("--lin-defl", "Linear deflection")->default_val(0.5)->check(CLI::Range(0.0, 1.0));
app.add_option("--ang-defl", "Angular deflection")->default_val(0.8)->check(CLI::Range(0.0, 1.0));
app.add_flag("--rel-defl", "Relative deflection");
app.add_flag("--debug", "Debug mode. Slower (and experimental), but provides more information about which STEP entities that failed to convert");
app.add_flag("--solid-only", "Solid only");
app.add_option("--max-geometry-num", "Maximum number of geometries to convert")->default_val(0);
app.add_option("--filter-names-include", "Include Filter name. Command separated list")->default_val("");
app.add_option("--filter-names-file-include", "Include Filter name file")->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("--tessellation-timeout", "Tessellation timeout")->default_val(30);
CLI11_PARSE(app, argc, argv);
const bool server_mode = app.get_option("--server")->as<bool>();
if (server_mode) {
GlobalConfig config;
config.linearDeflection = app.get_option("--lin-defl")->as<double>();
config.angularDeflection = app.get_option("--ang-defl")->as<double>();
config.relativeDeflection = app.get_option("--rel-defl")->as<bool>();
config.debug_mode = false;
config.solidOnly = false;
config.max_geometry_num = 0;
config.tessellation_timout = app.get_option("--tessellation-timeout")->as<int>();
config.serverConfig.enable_server = true;
config.serverConfig.port = app.get_option("--port")->as<int>();
config.serverConfig.host = app.get_option("--host")->as<std::string>();
config.serverConfig.max_file_size_mb = app.get_option("--max-file-size")->as<size_t>();
config.serverConfig.temp_dir = "./temp";
config.serverConfig.output_dir = "./output";
try {
start_http_server(config);
} catch (const std::exception& ex) {
std::cerr << "Server error: " << ex.what() << "\n";
return 1;
}
return 0;
}
app.get_option("--stp")->required();
app.get_option("--glb")->required();
GlobalConfig config;
try {
config = process_parameters(app);
} catch (const std::exception& ex) {
std::cerr << "Error: " << ex.what() << "\n";
return 1;
}
print_status(config);
std::cout << "\n";
std::cout << "Starting conversion..." << "\n";
const auto start = std::chrono::high_resolution_clock::now();
try {
if (config.buildConfig.build_bspline_surf)
make_a_bspline_surf(config);
if (config.debug_mode == 1)
debug_stp_to_glb(config);
else
{
convert_stp_to_glb(config);
}
} catch (std::exception& ex) {
std::cerr << "Error: " << ex.what() << "\n";
return 1;
}
const auto stop = std::chrono::high_resolution_clock::now();
const auto duration = std::chrono::duration_cast<std::chrono::microseconds>(stop - start);
const double seconds = static_cast<double>(duration.count()) / 1e6;
std::cout << "STP converted in: " << std::fixed << std::setprecision(2) << seconds << " seconds" << "\n";
return 0;
}

11911
src/third_party/httplib.h vendored Normal file

File diff suppressed because it is too large Load Diff