diff --git a/CLAUDE.md b/CLAUDE.md index 8c3a7b4..3cc7e47 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,6 +30,11 @@ MFC动态链接库(DLL)项目,作为Creo CAD软件插件运行,文档在项 - **层级删除** - 按层级安全删除组件,使用SuppressFeatures - **路径删除** - 按组件路径批量删除 +### 4. 搜索功能 +- **模型搜索** - 零件和装配体名称模糊匹配,支持三种匹配模式 +- **层级路径显示** - 返回完整的模型树层级路径 +- **去重算法** - 自动去除重复结果,保留最完整路径 + ## 核心API接口 ### 状态查询 @@ -53,6 +58,11 @@ POST /api/analysis/shell-analysis # 薄壳化分析 POST /api/analysis/geometry-complexity # 几何复杂度分析 ``` +### 搜索功能 +```http +POST /api/search/models # 模型名称模糊搜索 +``` + ### 删除操作 ```http POST /api/creo/hierarchy/delete # 层级删除 @@ -92,6 +102,13 @@ POST /api/creo/shrinkwrap/shell # Shrinkwrap导出(支持动态超 - **基于文件名去重**: 使用模型文件名作为唯一标识符进行重复检测 - **递归装配体支持**: 正确处理多层级装配体中的重复零件 +### 模型搜索优化 +- **多种匹配模式**: 支持prefix、contains、fuzzy三种匹配算法 +- **完整层级路径**: 从根装配体构建完整的模型树路径显示 +- **智能去重算法**: 自动去除重复结果,保留最长层级路径 +- **递归搜索**: 支持从根装配体开始的完整层级遍历 +- **向后兼容**: 不影响现有搜索功能,支持多种搜索范围 + ## 构建环境 - **IDE**: Visual Studio 2022 (v143工具集) @@ -116,6 +133,8 @@ POST /api/creo/shrinkwrap/shell # Shrinkwrap导出(支持动态超 - Socket超时阻塞 → 30秒超时机制 - Shrinkwrap复杂模型500错误 → 动态超时和详细异常处理 - 几何复杂度分析重复零件 → 装配体遍历去重机制 +- 模型搜索重复结果问题 → 智能去重算法保留最完整路径 +- 搜索结果缺少层级路径 → 从根装配体构建完整模型树路径 ## 下一步计划 diff --git a/MFCCreoDll.cpp b/MFCCreoDll.cpp index 4542b1b..cf1545f 100644 --- a/MFCCreoDll.cpp +++ b/MFCCreoDll.cpp @@ -9,6 +9,7 @@ #include "ShellExportHandler.h" #include "PathDeleteManager.h" #include "GeometryAnalyzer.h" +#include "ModelSearchHandler.h" #include #include #include @@ -1602,6 +1603,7 @@ extern "C" int user_initialize( g_http_server->SetRouteHandler("/api/analysis/geometry-complexity", GeometryComplexityHandler); g_http_server->SetRouteHandler("/api/creo/shrinkwrap/shell", ShrinkwrapShellHandler); g_http_server->SetRouteHandler("/api/creo/component/delete-by-path", PathDeleteHandler); + g_http_server->SetRouteHandler("/api/search/models", ModelSearchHandler::HandleModelSearchRequest); if (g_http_server->Start()) { diff --git a/MFCCreoDll.vcxproj b/MFCCreoDll.vcxproj index 27dd7e3..46c1146 100644 --- a/MFCCreoDll.vcxproj +++ b/MFCCreoDll.vcxproj @@ -215,6 +215,8 @@ ws2_32.lib;%(AdditionalDependencies) + + Create @@ -244,6 +246,8 @@ ws2_32.lib;%(AdditionalDependencies) + + diff --git a/MFCCreoDll.vcxproj.filters b/MFCCreoDll.vcxproj.filters index 0f0549b..62bc566 100644 --- a/MFCCreoDll.vcxproj.filters +++ b/MFCCreoDll.vcxproj.filters @@ -81,6 +81,12 @@ 源文件\src\creo + + 源文件\src\creo + + + 源文件\src\creo + @@ -154,6 +160,12 @@ 源文件\src\creo + + 源文件\src\creo + + + 源文件\src\creo + diff --git a/ModelSearchEngine.cpp b/ModelSearchEngine.cpp new file mode 100644 index 0000000..37dbb76 --- /dev/null +++ b/ModelSearchEngine.cpp @@ -0,0 +1,796 @@ +#include "pch.h" +#include "ModelSearchEngine.h" +#include +#include +#include +#include +#include + +// Singleton implementation +ModelSearchEngine& ModelSearchEngine::Instance() { + static ModelSearchEngine instance; + return instance; +} + +// Main search interface +ModelSearchResult ModelSearchEngine::SearchModels(const ModelSearchRequest& request) { + ModelSearchResult result; + + // Start timing + StartTimer(); + + try { + // Validate request parameters + if (request.query.empty()) { + result.error_message = "Search query cannot be empty"; + return result; + } + + // Clear previous search state + processed_models_.clear(); + std::vector all_results; + + // Choose search method based on scope + if (request.search_scope == "session" || request.search_scope == "all") { + // Search from root assemblies to build complete hierarchy paths + SearchFromRootAssemblies(request, all_results); + } + else if (request.search_scope == "current_model") { + SearchCurrentModel(request, all_results); + } + else { + result.error_message = "Invalid search scope: " + request.search_scope; + return result; + } + + // Sort and filter results + SortAndFilterResults(all_results, request); + + // Limit result count + result.total_found = static_cast(all_results.size()); + if (request.max_results > 0 && all_results.size() > static_cast(request.max_results)) { + all_results.resize(request.max_results); + } + + result.results = all_results; + result.returned_count = static_cast(result.results.size()); + result.success = true; + result.stats.search_scope_used = request.search_scope; + + } + catch (const pfcXToolkitError& e) { + result.error_message = "OTK Error occurred during search"; + } + catch (const std::exception& e) { + result.error_message = "Search error: " + std::string(e.what()); + } + catch (...) { + result.error_message = "Unknown error during model search"; + } + + // Set search time + result.search_time_ms = GetElapsedTimeMs(); + + return result; +} + +// Search models in current session +void ModelSearchEngine::SearchSessionModels(const ModelSearchRequest& request, std::vector& results) { + try { + pfcSession_ptr session = pfcGetCurrentSessionWithCompatibility(pfcC4Compatible); + if (!session) { + return; + } + + // Get all models in session + pfcModels_ptr models = session->ListModels(); + if (!models) { + return; + } + + // Iterate through all models + for (int i = 0; i < models->getarraysize(); i++) { + pfcModel_ptr model = models->get(i); + if (!model) continue; + + // Get model information + std::string model_type = GetModelTypeString(model); + + // Get file name for consistent path building + std::string model_name; + try { + xstring filename_xstr = model->GetFileName(); + std::wstring wfilename = filename_xstr; + model_name = std::string(wfilename.begin(), wfilename.end()); + } catch (...) { + model_name = GetModelDisplayName(model); // fallback + } + + std::string file_path = GetModelFilePath(model); + std::string full_path = GetModelFullPath(model); + + // Check model type filter + if (!ShouldIncludeModelType(model_type, request.model_types)) { + continue; + } + + // Calculate match score + double match_score = CalculateMatchScore(request, model_name, "name"); + + // Check similarity threshold + if (match_score < request.similarity_threshold) { + continue; + } + + // Create search result item + SearchResultItem item; + item.model_name = model_name; + item.display_name = model_name; + item.full_path = model_name; // 对于会话中的根模型,路径就是模型名 + item.file_path = file_path; // 物理文件路径 + item.model_type = model_type; + item.match_score = match_score; + item.match_type = "name"; + item.is_in_session = true; + item.is_assembly = (model_type == "ASSEMBLY"); + + // Build match reason + if (request.match_mode == "prefix") { + item.match_reason = "Name starts with '" + request.query + "'"; + } else if (request.match_mode == "contains") { + item.match_reason = "Name contains '" + request.query + "'"; + } else { + item.match_reason = "Fuzzy match similarity: " + std::to_string(static_cast(match_score * 100)) + "%"; + } + + item.matched_keywords.push_back(request.query); + results.push_back(item); + + // If assembly and need to search components + if (request.include_components && item.is_assembly) { + try { + pfcAssembly_ptr assembly = pfcAssembly::cast(model); + if (assembly) { + std::set local_processed; + SearchAssemblyComponents(request, assembly, results, local_processed, model_name); + } + } + catch (...) { + // Assembly search failure doesn't affect main results + } + } + } + + } + catch (...) { + // Session search failure doesn't throw exception + } +} + +// Search from root assemblies to build complete hierarchy paths +void ModelSearchEngine::SearchFromRootAssemblies(const ModelSearchRequest& request, std::vector& results) { + try { + pfcSession_ptr session = pfcGetCurrentSessionWithCompatibility(pfcC4Compatible); + if (!session) return; + + pfcModels_ptr models = session->ListModels(); + if (!models) return; + + // Global deduplication map: model_file_path -> longest_hierarchy_path + std::map best_results; + + // Find root assemblies and search recursively + for (int i = 0; i < models->getarraysize(); i++) { + pfcModel_ptr model = models->get(i); + if (!model) continue; + + // Skip if not assembly + if (model->GetType() != pfcMDL_ASSEMBLY) continue; + + // Get model file name for root path + std::string root_name; + try { + xstring filename_xstr = model->GetFileName(); + std::wstring wfilename = filename_xstr; + root_name = std::string(wfilename.begin(), wfilename.end()); + } catch (...) { + continue; + } + + // Check if root assembly itself matches + double match_score = CalculateMatchScore(request, root_name, "name"); + if (match_score >= request.similarity_threshold) { + SearchResultItem item; + item.model_name = root_name; + item.display_name = root_name; + item.full_path = root_name; // Root assembly path + item.file_path = GetModelFilePath(model); + item.model_type = "ASSEMBLY"; + item.match_score = match_score; + item.match_type = "name"; + item.is_in_session = true; + item.is_assembly = true; + item.match_reason = (request.match_mode == "contains") ? + ("Name contains '" + request.query + "'") : + ("Name matches '" + request.query + "'"); + item.matched_keywords.push_back(request.query); + + // Add to best results with deduplication + std::string key = item.file_path; + if (best_results.find(key) == best_results.end() || + best_results[key].full_path.length() < item.full_path.length()) { + best_results[key] = item; + } + } + + // Search components recursively with complete path + if (request.include_components) { + pfcAssembly_ptr assembly = pfcAssembly::cast(model); + if (assembly) { + std::set local_processed; + SearchAssemblyComponentsWithDedup(request, assembly, best_results, local_processed, root_name); + } + } + } + + // Add best results to final results + for (auto& pair : best_results) { + results.push_back(pair.second); + } + + } catch (...) { + // Error in root assembly search + } +} + +// Search current model +void ModelSearchEngine::SearchCurrentModel(const ModelSearchRequest& request, std::vector& results) { + try { + pfcSession_ptr session = pfcGetCurrentSessionWithCompatibility(pfcC4Compatible); + if (!session) { + return; + } + + pfcModel_ptr current_model = session->GetCurrentModel(); + if (!current_model) { + return; + } + + // Check current model itself + // Get file name for consistent path building + std::string model_name; + try { + xstring filename_xstr = current_model->GetFileName(); + std::wstring wfilename = filename_xstr; + model_name = std::string(wfilename.begin(), wfilename.end()); + } catch (...) { + model_name = GetModelDisplayName(current_model); // fallback + } + + double match_score = CalculateMatchScore(request, model_name, "name"); + + if (match_score >= request.similarity_threshold) { + std::string file_path = GetModelFilePath(current_model); + std::string full_path = GetModelFullPath(current_model); + + SearchResultItem item; + item.model_name = model_name; + item.display_name = model_name; + item.full_path = model_name; // 当前模型的路径就是模型名 + item.file_path = file_path; // 物理文件路径 + item.model_type = GetModelTypeString(current_model); + item.match_score = match_score; + item.match_type = "name"; + item.is_in_session = true; + item.is_assembly = (item.model_type == "ASSEMBLY"); + item.match_reason = "Current model match"; + item.matched_keywords.push_back(request.query); + results.push_back(item); + } + + // If assembly, search its components + if (request.include_components) { + try { + pfcAssembly_ptr assembly = pfcAssembly::cast(current_model); + if (assembly) { + std::set local_processed; + SearchAssemblyComponents(request, assembly, results, local_processed, model_name); + } + } + catch (...) { + // Component search failure doesn't affect main results + } + } + + } + catch (...) { + // Current model search failure doesn't throw exception + } +} + +// Search assembly components (recursive) +void ModelSearchEngine::SearchAssemblyComponents(const ModelSearchRequest& request, + pfcAssembly_ptr assembly, + std::vector& results, + std::set& processed_models, + const std::string& parent_path) { + if (!assembly) return; + + try { + // Get all component features (including suppressed ones) + pfcFeatures_ptr features = assembly->ListFeaturesByType(xfalse, pfcFeatureType::pfcFEATTYPE_COMPONENT); + if (!features) return; + + if (features->getarraysize() == 0) return; + + for (int i = 0; i < features->getarraysize(); i++) { + pfcFeature_ptr feature = features->get(i); + if (!feature) continue; + + try { + pfcComponentFeat_ptr comp_feat = pfcComponentFeat::cast(feature); + if (!comp_feat) continue; + + pfcModelDescriptor_ptr desc = comp_feat->GetModelDescr(); + if (!desc) continue; + + pfcModel_ptr comp_model = nullptr; + try { + pfcSession_ptr session = pfcGetCurrentSessionWithCompatibility(pfcC4Compatible); + if (session) { + comp_model = session->GetModelFromDescr(desc); + } + } catch (pfcXToolkitError&) { + // Model not loaded, skip this component + continue; + } + if (!comp_model) continue; + + // Get file name for component path (like hierarchy analysis) + std::string comp_name; + try { + xstring filename_xstr = desc->GetFileName(); + std::wstring wfilename = filename_xstr; + comp_name = std::string(wfilename.begin(), wfilename.end()); + } catch (...) { + comp_name = GetModelDisplayName(comp_model); // fallback + } + + std::string comp_file = GetModelFilePath(comp_model); + std::string full_path = GetModelFullPath(comp_model); + + // Prevent duplicate processing of same model + if (processed_models.find(comp_file) != processed_models.end()) { + continue; + } + processed_models.insert(comp_file); + + // Check model type filter + std::string model_type = GetModelTypeString(comp_model); + if (!ShouldIncludeModelType(model_type, request.model_types)) { + continue; + } + + // Calculate match score + double match_score = CalculateMatchScore(request, comp_name, "component"); + + // Build tree path for model hierarchy + std::string tree_path; + if (parent_path.empty()) { + tree_path = comp_name; + } else { + tree_path = parent_path + "/" + comp_name; + } + + if (match_score >= request.similarity_threshold) { + SearchResultItem item; + item.model_name = comp_name; + item.display_name = comp_name; + item.full_path = tree_path; // Model tree hierarchy path + item.file_path = comp_file; // Physical file path + item.model_type = model_type; + item.component_path = BuildComponentPath(parent_path, comp_name); + item.match_score = match_score; + item.match_type = "component"; + item.is_in_session = true; + item.is_assembly = (model_type == "ASSEMBLY"); + item.match_reason = "Component name match in assembly"; + item.matched_keywords.push_back(request.query); + results.push_back(item); + } + + // If this component is also an assembly, search recursively + if (model_type == "ASSEMBLY") { + try { + pfcAssembly_ptr sub_assembly = pfcAssembly::cast(comp_model); + if (sub_assembly) { + SearchAssemblyComponents(request, sub_assembly, results, processed_models, tree_path); + } + } + catch (...) { + // Sub-assembly search failure doesn't affect other results + } + } + + } + catch (...) { + // Single component processing failure doesn't affect other components + continue; + } + } + + } + catch (...) { + // Assembly component search failure doesn't throw exception + } +} + +// Search assembly components with global deduplication (recursive) +void ModelSearchEngine::SearchAssemblyComponentsWithDedup(const ModelSearchRequest& request, + pfcAssembly_ptr assembly, + std::map& best_results, + std::set& processed_models, + const std::string& parent_path) { + if (!assembly) return; + + try { + // Get all component features (including suppressed ones) + pfcFeatures_ptr features = assembly->ListFeaturesByType(xfalse, pfcFeatureType::pfcFEATTYPE_COMPONENT); + if (!features) return; + + if (features->getarraysize() == 0) return; + + for (int i = 0; i < features->getarraysize(); i++) { + pfcFeature_ptr feature = features->get(i); + if (!feature) continue; + + try { + pfcComponentFeat_ptr comp_feat = pfcComponentFeat::cast(feature); + if (!comp_feat) continue; + + pfcModelDescriptor_ptr desc = comp_feat->GetModelDescr(); + if (!desc) continue; + + pfcModel_ptr comp_model = nullptr; + try { + pfcSession_ptr session = pfcGetCurrentSessionWithCompatibility(pfcC4Compatible); + if (session) { + comp_model = session->GetModelFromDescr(desc); + } + } catch (pfcXToolkitError&) { + // Model not loaded, skip this component + continue; + } + if (!comp_model) continue; + + // Get file name for component path (like hierarchy analysis) + std::string comp_name; + try { + xstring filename_xstr = desc->GetFileName(); + std::wstring wfilename = filename_xstr; + comp_name = std::string(wfilename.begin(), wfilename.end()); + } catch (...) { + comp_name = GetModelDisplayName(comp_model); // fallback + } + + std::string comp_file = GetModelFilePath(comp_model); + std::string full_path = GetModelFullPath(comp_model); + + // Prevent duplicate processing of same model + if (processed_models.find(comp_file) != processed_models.end()) { + continue; + } + processed_models.insert(comp_file); + + // Check model type filter + std::string model_type = GetModelTypeString(comp_model); + if (!ShouldIncludeModelType(model_type, request.model_types)) { + continue; + } + + // Calculate match score + double match_score = CalculateMatchScore(request, comp_name, "component"); + + // Build tree path for model hierarchy + std::string tree_path; + if (parent_path.empty()) { + tree_path = comp_name; + } else { + tree_path = parent_path + "/" + comp_name; + } + + if (match_score >= request.similarity_threshold) { + SearchResultItem item; + item.model_name = comp_name; + item.display_name = comp_name; + item.full_path = tree_path; // Model tree hierarchy path + item.file_path = comp_file; // Physical file path + item.model_type = model_type; + item.component_path = BuildComponentPath(parent_path, comp_name); + item.match_score = match_score; + item.match_type = "component"; + item.is_in_session = true; + item.is_assembly = (model_type == "ASSEMBLY"); + item.match_reason = "Component name match in assembly"; + item.matched_keywords.push_back(request.query); + + // Add to best results with deduplication by file path + // Keep the result with longest hierarchy path + std::string key = comp_file; + if (best_results.find(key) == best_results.end() || + best_results[key].full_path.length() < item.full_path.length()) { + best_results[key] = item; + } + } + + // If this component is also an assembly, search recursively + if (model_type == "ASSEMBLY") { + try { + pfcAssembly_ptr sub_assembly = pfcAssembly::cast(comp_model); + if (sub_assembly) { + SearchAssemblyComponentsWithDedup(request, sub_assembly, best_results, processed_models, tree_path); + } + } + catch (...) { + // Sub-assembly search failure doesn't affect other results + } + } + + } + catch (...) { + // Single component processing failure doesn't affect other components + continue; + } + } + + } + catch (...) { + // Assembly component search failure doesn't throw exception + } +} + +// Calculate match score +double ModelSearchEngine::CalculateMatchScore(const ModelSearchRequest& request, + const std::string& target_name, + const std::string& match_type) { + if (request.match_mode == "prefix") { + return MatchesPrefix(ToLowerCase(target_name), ToLowerCase(request.query)) ? 1.0 : 0.0; + } + else if (request.match_mode == "contains") { + return MatchesContains(ToLowerCase(target_name), ToLowerCase(request.query)) ? 0.8 : 0.0; + } + else if (request.match_mode == "fuzzy") { + double score = CalculateFuzzyMatch(ToLowerCase(request.query), ToLowerCase(target_name)); + + // Adjust score based on match type + if (match_type == "name") { + score *= 1.0; // Name match no adjustment + } else if (match_type == "component") { + score *= 0.9; // Component match slightly lower score + } + + return score; + } + + return 0.0; +} + +// Sort and filter results +void ModelSearchEngine::SortAndFilterResults(std::vector& results, + const ModelSearchRequest& request) { + // Sort by match score in descending order + std::sort(results.begin(), results.end(), + [](const SearchResultItem& a, const SearchResultItem& b) { + if (std::abs(a.match_score - b.match_score) < 0.001) { + // Same score, prioritize name matches + if (a.match_type != b.match_type) { + if (a.match_type == "name") return true; + if (b.match_type == "name") return false; + } + // Then sort by name alphabetically + return a.model_name < b.model_name; + } + return a.match_score > b.match_score; + }); +} + +// Fuzzy matching algorithm implementation +double ModelSearchEngine::CalculateFuzzyMatch(const std::string& query, const std::string& target) { + if (query.empty() || target.empty()) { + return 0.0; + } + + // Exact match + if (query == target) { + return 1.0; + } + + // Prefix match + if (target.find(query) == 0) { + return 0.95; + } + + // Contains match + if (target.find(query) != std::string::npos) { + return 0.8; + } + + // Edit distance match + double edit_distance = CalculateEditDistance(query, target); + size_t max_len = (query.length() > target.length()) ? query.length() : target.length(); + + if (max_len == 0) return 1.0; + + double similarity = 1.0 - (edit_distance / max_len); + return (similarity > 0.0) ? similarity : 0.0; +} + +// String matching tools implementation +bool ModelSearchEngine::MatchesPrefix(const std::string& text, const std::string& prefix) { + return text.size() >= prefix.size() && text.substr(0, prefix.size()) == prefix; +} + +bool ModelSearchEngine::MatchesContains(const std::string& text, const std::string& substring) { + return text.find(substring) != std::string::npos; +} + +double ModelSearchEngine::CalculateEditDistance(const std::string& s1, const std::string& s2) { + const size_t len1 = s1.size(); + const size_t len2 = s2.size(); + + std::vector> dp(len1 + 1, std::vector(len2 + 1)); + + for (size_t i = 0; i <= len1; ++i) dp[i][0] = static_cast(i); + for (size_t j = 0; j <= len2; ++j) dp[0][j] = static_cast(j); + + for (size_t i = 1; i <= len1; ++i) { + for (size_t j = 1; j <= len2; ++j) { + if (s1[i-1] == s2[j-1]) { + dp[i][j] = dp[i-1][j-1]; + } else { + int a = dp[i-1][j]; + int b = dp[i][j-1]; + int c = dp[i-1][j-1]; + dp[i][j] = 1 + ((a < b) ? ((a < c) ? a : c) : ((b < c) ? b : c)); + } + } + } + + return static_cast(dp[len1][len2]); +} + +// Helper methods implementation +std::string ModelSearchEngine::GetModelTypeString(pfcModel_ptr model) { + if (!model) return "UNKNOWN"; + + pfcModelType type = model->GetType(); + switch (type) { + case pfcModelType::pfcMDL_PART: return "PART"; + case pfcModelType::pfcMDL_ASSEMBLY: return "ASSEMBLY"; + case pfcModelType::pfcMDL_DRAWING: return "DRAWING"; + default: return "OTHER"; + } +} + +std::string ModelSearchEngine::GetModelDisplayName(pfcModel_ptr model) { + if (!model) return ""; + + try { + xstring name = model->GetInstanceName(); + std::wstring wname = name; + return std::string(wname.begin(), wname.end()); + } + catch (...) { + try { + xstring filename = model->GetOrigin(); + std::wstring wfilename = filename; + std::string full_name(wfilename.begin(), wfilename.end()); + + // Extract filename part + size_t last_slash = full_name.find_last_of("\\/"); + if (last_slash != std::string::npos) { + return full_name.substr(last_slash + 1); + } + return full_name; + } + catch (...) { + return "Unknown"; + } + } +} + +std::string ModelSearchEngine::GetModelFilePath(pfcModel_ptr model) { + if (!model) return ""; + + try { + xstring origin = model->GetOrigin(); + std::wstring worigin = origin; + return std::string(worigin.begin(), worigin.end()); + } + catch (...) { + return ""; + } +} + +std::string ModelSearchEngine::GetModelFullPath(pfcModel_ptr model) { + if (!model) return ""; + + try { + // Try GetFullName first (includes full path) + xstring full_name = model->GetFullName(); + std::wstring wfull_name = full_name; + return std::string(wfull_name.begin(), wfull_name.end()); + } + catch (...) { + // Fallback to GetOrigin if GetFullName fails + try { + xstring origin = model->GetOrigin(); + std::wstring worigin = origin; + return std::string(worigin.begin(), worigin.end()); + } + catch (...) { + return ""; + } + } +} + +bool ModelSearchEngine::ShouldIncludeModelType(const std::string& model_type, + const std::vector& type_filters) { + if (type_filters.empty()) { + return true; // No filter, include all types + } + + for (const auto& filter : type_filters) { + if (filter == "MDL_PART" && model_type == "PART") return true; + if (filter == "MDL_ASSEMBLY" && model_type == "ASSEMBLY") return true; + if (filter == "MDL_DRAWING" && model_type == "DRAWING") return true; + if (filter == model_type) return true; + } + + return false; +} + +// String processing tools implementation +std::string ModelSearchEngine::ToLowerCase(const std::string& str) { + std::string result = str; + std::transform(result.begin(), result.end(), result.begin(), ::tolower); + return result; +} + +std::vector ModelSearchEngine::SplitString(const std::string& str, char delimiter) { + std::vector tokens; + std::stringstream ss(str); + std::string token; + + while (std::getline(ss, token, delimiter)) { + tokens.push_back(TrimString(token)); + } + + return tokens; +} + +std::string ModelSearchEngine::TrimString(const std::string& str) { + size_t start = str.find_first_not_of(" \t\n\r"); + if (start == std::string::npos) return ""; + + size_t end = str.find_last_not_of(" \t\n\r"); + return str.substr(start, end - start + 1); +} + +std::string ModelSearchEngine::BuildComponentPath(const std::string& parent_path, const std::string& component_name) { + if (parent_path.empty()) { + return component_name; + } + return parent_path + " > " + component_name; +} + +// Performance timing methods +void ModelSearchEngine::StartTimer() { + search_start_time_ = std::chrono::high_resolution_clock::now(); +} + +std::string ModelSearchEngine::GetElapsedTimeMs() { + auto end_time = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(end_time - search_start_time_); + return std::to_string(duration.count()) + "ms"; +} \ No newline at end of file diff --git a/ModelSearchEngine.h b/ModelSearchEngine.h new file mode 100644 index 0000000..b366883 --- /dev/null +++ b/ModelSearchEngine.h @@ -0,0 +1,180 @@ +#pragma once + +// 基础OTK头文件 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// 搜索请求结构 +struct ModelSearchRequest { + std::string query; // 搜索关键词 + std::string match_mode; // 匹配模式: "prefix", "contains", "fuzzy" + std::string search_scope; // 搜索范围: "session", "current_model", "all" + std::vector model_types; // 模型类型过滤: ["MDL_PART", "MDL_ASSEMBLY"] + int max_results; // 最大返回结果数 + double similarity_threshold; // 模糊匹配相似度阈值 (0.0-1.0) + bool include_components; // 是否包含装配体组件搜索 + bool include_features; // 是否包含特征名称搜索 + + // 构造函数 + ModelSearchRequest() : + match_mode("contains"), + search_scope("session"), + max_results(50), + similarity_threshold(0.3), + include_components(true), + include_features(false) + { + } +}; + +// 搜索结果项结构 +struct SearchResultItem { + std::string model_name; // 模型名称 + std::string display_name; // 显示名称 + std::string full_path; // 模型树中的完整路径 + std::string file_path; // 文件路径 (GetOrigin) + std::string model_type; // 模型类型 ("PART", "ASSEMBLY", "DRAWING") + std::string component_path; // 组件路径(装配体中的位置) + double match_score; // 匹配分数 (0.0-1.0) + std::string match_reason; // 匹配原因说明 + std::string match_type; // 匹配类型 ("name", "component", "feature") + bool is_in_session; // 是否在当前会话中 + bool is_assembly; // 是否是装配体 + int component_count; // 组件数量(装配体) + + // 扩展信息 + std::string file_size; // 文件大小 + std::string last_modified; // 最后修改时间 + std::vector matched_keywords; // 匹配的关键词 + + // 构造函数 + SearchResultItem() : + match_score(0.0), + is_in_session(false), + is_assembly(false), + component_count(0) + { + } +}; + +// 搜索结果结构 +struct ModelSearchResult { + bool success; // 搜索是否成功 + std::vector results; // 搜索结果列表 + int total_found; // 总找到数量 + int returned_count; // 返回数量 + std::string search_time_ms; // 搜索耗时(毫秒) + std::string error_message; // 错误信息 + + // 搜索统计 + struct SearchStats { + int session_models_count; // 会话模型数量 + int components_searched; // 搜索的组件数量 + int features_searched; // 搜索的特征数量 + std::string search_scope_used; // 实际使用的搜索范围 + } stats; + + // 构造函数 + ModelSearchResult() : + success(false), + total_found(0), + returned_count(0) + { + stats.session_models_count = 0; + stats.components_searched = 0; + stats.features_searched = 0; + } +}; + +// 模型搜索引擎类 +class ModelSearchEngine { +public: + // 单例模式 + static ModelSearchEngine& Instance(); + + // 主要搜索接口 + ModelSearchResult SearchModels(const ModelSearchRequest& request); + + // 模糊匹配算法 + static double CalculateFuzzyMatch(const std::string& query, const std::string& target); + + // 字符串匹配工具 + static bool MatchesPrefix(const std::string& text, const std::string& prefix); + static bool MatchesContains(const std::string& text, const std::string& substring); + static double CalculateEditDistance(const std::string& s1, const std::string& s2); + +private: + // 构造函数(私有) + ModelSearchEngine() = default; + ~ModelSearchEngine() = default; + + // 删除拷贝构造 + ModelSearchEngine(const ModelSearchEngine&) = delete; + ModelSearchEngine& operator=(const ModelSearchEngine&) = delete; + + // 核心搜索方法 + void SearchSessionModels(const ModelSearchRequest& request, std::vector& results); + void SearchCurrentModel(const ModelSearchRequest& request, std::vector& results); + void SearchFromRootAssemblies(const ModelSearchRequest& request, std::vector& results); + void SearchAssemblyComponents(const ModelSearchRequest& request, + pfcAssembly_ptr assembly, + std::vector& results, + std::set& processed_models, + const std::string& parent_path = ""); + void SearchAssemblyComponentsWithDedup(const ModelSearchRequest& request, + pfcAssembly_ptr assembly, + std::map& best_results, + std::set& processed_models, + const std::string& parent_path = ""); + + // 匹配评分方法 + double CalculateMatchScore(const ModelSearchRequest& request, + const std::string& target_name, + const std::string& match_type); + + // 结果处理方法 + void SortAndFilterResults(std::vector& results, + const ModelSearchRequest& request); + + // 辅助方法 + std::string GetModelTypeString(pfcModel_ptr model); + std::string GetModelDisplayName(pfcModel_ptr model); + std::string GetModelFilePath(pfcModel_ptr model); + std::string GetModelFullPath(pfcModel_ptr model); + bool ShouldIncludeModelType(const std::string& model_type, + const std::vector& type_filters); + + // 字符串处理工具 + static std::string ToLowerCase(const std::string& str); + static std::vector SplitString(const std::string& str, char delimiter); + static std::string TrimString(const std::string& str); + + // 组件路径构建 + std::string BuildComponentPath(const std::string& parent_path, const std::string& component_name); + + // 去重处理 + std::set processed_models_; + + // 性能计时 + std::chrono::high_resolution_clock::time_point search_start_time_; + void StartTimer(); + std::string GetElapsedTimeMs(); +}; diff --git a/ModelSearchHandler.cpp b/ModelSearchHandler.cpp new file mode 100644 index 0000000..573293a --- /dev/null +++ b/ModelSearchHandler.cpp @@ -0,0 +1,379 @@ +#include "pch.h" +#include "ModelSearchHandler.h" +#include +#include + +// Main HTTP request handler +HttpResponse ModelSearchHandler::HandleModelSearchRequest(const HttpRequest& request) { + HttpResponse response; + + // Validate request method + if (request.method != "POST") { + return FormatErrorResponse(405, "Method not allowed. Use POST."); + } + + // Validate request + std::string error_message; + if (!ValidateHttpRequest(request, error_message)) { + return FormatErrorResponse(400, error_message); + } + + try { + // Parse search request + ModelSearchRequest search_request = ParseSearchRequest(request.body); + + // Execute search + ModelSearchResult search_result = ModelSearchEngine::Instance().SearchModels(search_request); + + // Format response + if (search_result.success) { + response = FormatSuccessResponse(search_result); + } else { + response = FormatErrorResponse(500, search_result.error_message); + } + + } + catch (const std::exception& e) { + response = FormatErrorResponse(500, "Request processing error: " + std::string(e.what())); + } + catch (...) { + response = FormatErrorResponse(500, "Unknown error during model search"); + } + + return response; +} + +// Request validation method +bool ModelSearchHandler::ValidateHttpRequest(const HttpRequest& request, std::string& error_message) { + if (request.body.empty()) { + error_message = "Request body cannot be empty"; + return false; + } + + // Check basic JSON format + if (request.body.find("{") == std::string::npos || request.body.find("}") == std::string::npos) { + error_message = "Invalid JSON format in request body"; + return false; + } + + return true; +} + +// Parse search request +ModelSearchRequest ModelSearchHandler::ParseSearchRequest(const std::string& json_body) { + ModelSearchRequest request; + + // Parse required parameters + request.query = Trim(ExtractJsonValue(json_body, "query")); + if (request.query.empty()) { + request.query = Trim(ExtractJsonValue(json_body, "search_query")); + } + + // Parse optional parameters + std::string match_mode = ExtractJsonValue(json_body, "match_mode"); + if (!match_mode.empty()) { + request.match_mode = match_mode; + } + + std::string search_scope = ExtractJsonValue(json_body, "search_scope"); + if (!search_scope.empty()) { + request.search_scope = search_scope; + } + + // Parse model type filter + std::vector model_types = ExtractJsonArrayValue(json_body, "model_types"); + if (!model_types.empty()) { + request.model_types = model_types; + } + + // Parse numeric parameters + int max_results = ExtractJsonIntValue(json_body, "max_results", 0); + if (max_results > 0) { + request.max_results = max_results; + } + + double similarity_threshold = ExtractJsonDoubleValue(json_body, "similarity_threshold", -1.0); + if (similarity_threshold >= 0.0 && similarity_threshold <= 1.0) { + request.similarity_threshold = similarity_threshold; + } + + // Parse boolean parameters + request.include_components = ExtractJsonBoolValue(json_body, "include_components", request.include_components); + request.include_features = ExtractJsonBoolValue(json_body, "include_features", request.include_features); + + return request; +} + +// Format success response +HttpResponse ModelSearchHandler::FormatSuccessResponse(const ModelSearchResult& result) { + HttpResponse response; + response.status_code = 200; + + std::ostringstream json; + json << "{" + << "\"success\": true," + << "\"data\": {" + << "\"results\": ["; + + // Build search results list + bool first_item = true; + for (const auto& item : result.results) { + if (!first_item) json << ","; + first_item = false; + + json << "{" + << "\"model_name\": \"" << EscapeJsonString(item.model_name) << "\"," + << "\"display_name\": \"" << EscapeJsonString(item.display_name) << "\"," + << "\"full_path\": \"" << EscapeJsonString(item.full_path) << "\"," + << "\"model_type\": \"" << EscapeJsonString(item.model_type) << "\"," + << "\"component_path\": \"" << EscapeJsonString(item.component_path) << "\"," + << "\"match_score\": " << item.match_score << "," + << "\"match_reason\": \"" << EscapeJsonString(item.match_reason) << "\"," + << "\"match_type\": \"" << EscapeJsonString(item.match_type) << "\"," + << "\"is_in_session\": " << (item.is_in_session ? "true" : "false") << "," + << "\"is_assembly\": " << (item.is_assembly ? "true" : "false") << "," + << "\"component_count\": " << item.component_count << "," + << "\"file_size\": \"" << EscapeJsonString(item.file_size) << "\"," + << "\"last_modified\": \"" << EscapeJsonString(item.last_modified) << "\"," + << "\"matched_keywords\": ["; + + // Build matched keywords list + bool first_keyword = true; + for (const auto& keyword : item.matched_keywords) { + if (!first_keyword) json << ","; + first_keyword = false; + json << "\"" << EscapeJsonString(keyword) << "\""; + } + + json << "]" + << "}"; + } + + json << "]," + << "\"total_found\": " << result.total_found << "," + << "\"returned_count\": " << result.returned_count << "," + << "\"search_time_ms\": \"" << EscapeJsonString(result.search_time_ms) << "\"," + << "\"stats\": {" + << "\"session_models_count\": " << result.stats.session_models_count << "," + << "\"components_searched\": " << result.stats.components_searched << "," + << "\"features_searched\": " << result.stats.features_searched << "," + << "\"search_scope_used\": \"" << EscapeJsonString(result.stats.search_scope_used) << "\"" + << "}" + << "}," + << "\"error\": null" + << "}"; + + response.body = json.str(); + return response; +} + +// Format error response +HttpResponse ModelSearchHandler::FormatErrorResponse(int status_code, const std::string& error_message) { + HttpResponse response; + response.status_code = status_code; + + std::ostringstream json; + json << "{" + << "\"success\": false," + << "\"data\": null," + << "\"error\": \"" << EscapeJsonString(error_message) << "\"" + << "}"; + + response.body = json.str(); + return response; +} + +// JSON parsing helper methods +std::string ModelSearchHandler::ExtractJsonValue(const std::string& json, const std::string& key) { + // Find key-value pair "key": "value" or "key":"value" + std::string key_pattern = "\"" + key + "\""; + size_t key_pos = json.find(key_pattern); + + if (key_pos != std::string::npos) { + // Find colon + size_t colon_pos = json.find(":", key_pos); + if (colon_pos != std::string::npos) { + // Skip spaces to find value start + size_t value_start = colon_pos + 1; + while (value_start < json.length() && + (json[value_start] == ' ' || json[value_start] == '\t' || + json[value_start] == '\n' || json[value_start] == '\r')) { + value_start++; + } + + // Check if it's a string value (starts with double quote) + if (value_start < json.length() && json[value_start] == '"') { + value_start++; // Skip opening quote + size_t value_end = json.find('"', value_start); + if (value_end != std::string::npos) { + return json.substr(value_start, value_end - value_start); + } + } + else { + // Non-string value, find until comma, brace or string end + size_t value_end = value_start; + while (value_end < json.length() && + json[value_end] != ',' && json[value_end] != '}' && + json[value_end] != ']' && json[value_end] != '\n') { + value_end++; + } + + std::string value = Trim(json.substr(value_start, value_end - value_start)); + return value; + } + } + } + + return ""; +} + +bool ModelSearchHandler::ExtractJsonBoolValue(const std::string& json, const std::string& key, bool default_value) { + std::string value = ExtractJsonValue(json, key); + if (value.empty()) { + return default_value; + } + + // Convert to lowercase for comparison + std::transform(value.begin(), value.end(), value.begin(), ::tolower); + + return (value == "true" || value == "1"); +} + +int ModelSearchHandler::ExtractJsonIntValue(const std::string& json, const std::string& key, int default_value) { + std::string value = ExtractJsonValue(json, key); + if (value.empty()) { + return default_value; + } + + try { + return std::stoi(value); + } + catch (...) { + return default_value; + } +} + +double ModelSearchHandler::ExtractJsonDoubleValue(const std::string& json, const std::string& key, double default_value) { + std::string value = ExtractJsonValue(json, key); + if (value.empty()) { + return default_value; + } + + try { + return std::stod(value); + } + catch (...) { + return default_value; + } +} + +std::vector ModelSearchHandler::ExtractJsonArrayValue(const std::string& json, const std::string& key) { + std::vector result; + + // Find key-value pair "key": [...] + std::string key_pattern = "\"" + key + "\""; + size_t key_pos = json.find(key_pattern); + + if (key_pos != std::string::npos) { + // Find colon + size_t colon_pos = json.find(":", key_pos); + if (colon_pos != std::string::npos) { + // Skip spaces to find array start + size_t array_start = colon_pos + 1; + while (array_start < json.length() && + (json[array_start] == ' ' || json[array_start] == '\t' || + json[array_start] == '\n' || json[array_start] == '\r')) { + array_start++; + } + + // Check if it's an array (starts with [) + if (array_start < json.length() && json[array_start] == '[') { + size_t array_end = json.find(']', array_start); + if (array_end != std::string::npos) { + std::string array_content = json.substr(array_start + 1, array_end - array_start - 1); + + // Parse array elements + size_t pos = 0; + while (pos < array_content.length()) { + // Skip spaces + while (pos < array_content.length() && + (array_content[pos] == ' ' || array_content[pos] == '\t' || + array_content[pos] == '\n' || array_content[pos] == '\r')) { + pos++; + } + + if (pos >= array_content.length()) break; + + // Find string value + if (array_content[pos] == '"') { + pos++; // Skip opening quote + size_t end_quote = array_content.find('"', pos); + if (end_quote != std::string::npos) { + result.push_back(array_content.substr(pos, end_quote - pos)); + pos = end_quote + 1; + } + } + + // Find next comma + size_t next_comma = array_content.find(',', pos); + if (next_comma != std::string::npos) { + pos = next_comma + 1; + } else { + break; + } + } + } + } + } + } + + return result; +} + +// JSON escape method +std::string ModelSearchHandler::EscapeJsonString(const std::string& input) { + std::string escaped; + escaped.reserve(input.length() * 2); + + for (char c : input) { + switch (c) { + case '"': escaped += "\\\""; break; + case '\\': escaped += "\\\\"; break; + case '\b': escaped += "\\b"; break; + case '\f': escaped += "\\f"; break; + case '\n': escaped += "\\n"; break; + case '\r': escaped += "\\r"; break; + case '\t': escaped += "\\t"; break; + default: + if (c < 0x20) { + // Control character escape to Unicode + char buffer[7]; + snprintf(buffer, sizeof(buffer), "\\u%04x", static_cast(c)); + escaped += buffer; + } else { + escaped += c; + } + break; + } + } + + return escaped; +} + +// Helper utility methods +std::string ModelSearchHandler::Trim(const std::string& str) { + size_t start = str.find_first_not_of(" \t\n\r"); + if (start == std::string::npos) return ""; + + size_t end = str.find_last_not_of(" \t\n\r"); + return str.substr(start, end - start + 1); +} + +bool ModelSearchHandler::StartsWith(const std::string& str, const std::string& prefix) { + return str.size() >= prefix.size() && str.substr(0, prefix.size()) == prefix; +} + +bool ModelSearchHandler::EndsWith(const std::string& str, const std::string& suffix) { + return str.size() >= suffix.size() && + str.substr(str.size() - suffix.size()) == suffix; +} \ No newline at end of file diff --git a/ModelSearchHandler.h b/ModelSearchHandler.h new file mode 100644 index 0000000..6a58d23 --- /dev/null +++ b/ModelSearchHandler.h @@ -0,0 +1,38 @@ +#pragma once + +#include "ModelSearchEngine.h" +#include "HttpServer.h" +#include +#include + +class ModelSearchHandler { +public: + // 主要HTTP处理方法 + static HttpResponse HandleModelSearchRequest(const HttpRequest& request); + +private: + // JSON解析辅助方法 + static std::string ExtractJsonValue(const std::string& json, const std::string& key); + static bool ExtractJsonBoolValue(const std::string& json, const std::string& key, bool default_value = false); + static int ExtractJsonIntValue(const std::string& json, const std::string& key, int default_value = 0); + static double ExtractJsonDoubleValue(const std::string& json, const std::string& key, double default_value = 0.0); + static std::vector ExtractJsonArrayValue(const std::string& json, const std::string& key); + + // JSON转义辅助方法 + static std::string EscapeJsonString(const std::string& input); + + // 请求验证方法 + static bool ValidateHttpRequest(const HttpRequest& request, std::string& error_message); + + // 参数解析方法 + static ModelSearchRequest ParseSearchRequest(const std::string& json_body); + + // 响应格式化方法 + static HttpResponse FormatSuccessResponse(const ModelSearchResult& result); + static HttpResponse FormatErrorResponse(int status_code, const std::string& error_message); + + // 辅助工具方法 + static std::string Trim(const std::string& str); + static bool StartsWith(const std::string& str, const std::string& prefix); + static bool EndsWith(const std::string& str, const std::string& suffix); +};