实现模型搜索功能 - 支持名称模糊匹配和完整层级路径显示

新增ModelSearchEngine和ModelSearchHandler模块,提供零件和装配体名称搜索功能:
• 支持prefix、contains、fuzzy三种匹配模式
• 从根装配体构建完整模型树层级路径
• 智能去重算法自动去除重复结果,保留最长路径
• 递归搜索支持多层级装配体遍历
• 向后兼容,不影响现有功能

解决搜索重复结果和缺少层级路径问题,提升用户体验。

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
sladro 2025-08-28 18:36:12 +08:00
parent d5accfe24a
commit b874b17534
8 changed files with 1430 additions and 0 deletions

View File

@ -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错误 → 动态超时和详细异常处理
- 几何复杂度分析重复零件 → 装配体遍历去重机制
- 模型搜索重复结果问题 → 智能去重算法保留最完整路径
- 搜索结果缺少层级路径 → 从根装配体构建完整模型树路径
## 下一步计划

View File

@ -9,6 +9,7 @@
#include "ShellExportHandler.h"
#include "PathDeleteManager.h"
#include "GeometryAnalyzer.h"
#include "ModelSearchHandler.h"
#include <wfcSession.h>
#include <wfcGlobal.h>
#include <string>
@ -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()) {

View File

@ -215,6 +215,8 @@ ws2_32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<ClCompile Include="Logger.cpp" />
<ClCompile Include="MFCCreoDll.cpp" />
<ClCompile Include="ModelAnalyzer.cpp" />
<ClCompile Include="ModelSearchEngine.cpp" />
<ClCompile Include="ModelSearchHandler.cpp" />
<ClCompile Include="PathDeleteManager.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
@ -244,6 +246,8 @@ ws2_32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<ClInclude Include="Logger.h" />
<ClInclude Include="MFCCreoDll.h" />
<ClInclude Include="ModelAnalyzer.h" />
<ClInclude Include="ModelSearchEngine.h" />
<ClInclude Include="ModelSearchHandler.h" />
<ClInclude Include="PathDeleteManager.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="pfcExceptions.h" />

View File

@ -81,6 +81,12 @@
<ClCompile Include="GeometryAnalyzer.cpp">
<Filter>源文件\src\creo</Filter>
</ClCompile>
<ClCompile Include="ModelSearchHandler.cpp">
<Filter>源文件\src\creo</Filter>
</ClCompile>
<ClCompile Include="ModelSearchEngine.cpp">
<Filter>源文件\src\creo</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="MFCCreoDll.def">
@ -154,6 +160,12 @@
<ClInclude Include="GeometryAnalyzer.h">
<Filter>源文件\src\creo</Filter>
</ClInclude>
<ClInclude Include="ModelSearchHandler.h">
<Filter>源文件\src\creo</Filter>
</ClInclude>
<ClInclude Include="ModelSearchEngine.h">
<Filter>源文件\src\creo</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="MFCCreoDll.rc">

796
ModelSearchEngine.cpp Normal file
View File

@ -0,0 +1,796 @@
#include "pch.h"
#include "ModelSearchEngine.h"
#include <sstream>
#include <iostream>
#include <iomanip>
#include <cctype>
#include <algorithm>
// 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<SearchResultItem> 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<int>(all_results.size());
if (request.max_results > 0 && all_results.size() > static_cast<size_t>(request.max_results)) {
all_results.resize(request.max_results);
}
result.results = all_results;
result.returned_count = static_cast<int>(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<SearchResultItem>& 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<int>(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<std::string> 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<SearchResultItem>& 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<std::string, SearchResultItem> 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<std::string> 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<SearchResultItem>& 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<std::string> 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<SearchResultItem>& results,
std::set<std::string>& 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<std::string, SearchResultItem>& best_results,
std::set<std::string>& 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<SearchResultItem>& 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<std::vector<int>> dp(len1 + 1, std::vector<int>(len2 + 1));
for (size_t i = 0; i <= len1; ++i) dp[i][0] = static_cast<int>(i);
for (size_t j = 0; j <= len2; ++j) dp[0][j] = static_cast<int>(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<double>(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<std::string>& 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<std::string> ModelSearchEngine::SplitString(const std::string& str, char delimiter) {
std::vector<std::string> 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<std::chrono::milliseconds>(end_time - search_start_time_);
return std::to_string(duration.count()) + "ms";
}

180
ModelSearchEngine.h Normal file
View File

@ -0,0 +1,180 @@
#pragma once
// 基础OTK头文件
#include <pfcGlobal.h>
#include <pfcSession.h>
#include <wfcSession.h>
#include <wfcGlobal.h>
#include <pfcModel.h>
#include <pfcExceptions.h>
#include <pfcFeature.h>
#include <pfcComponentFeat.h>
#include <pfcSolid.h>
#include <wfcSolid.h>
#include <pfcAssembly.h>
#include <wfcAssembly.h>
#include <string>
#include <vector>
#include <set>
#include <map>
#include <algorithm>
#include <cmath>
#include <memory>
#include <chrono>
// 搜索请求结构
struct ModelSearchRequest {
std::string query; // 搜索关键词
std::string match_mode; // 匹配模式: "prefix", "contains", "fuzzy"
std::string search_scope; // 搜索范围: "session", "current_model", "all"
std::vector<std::string> 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<std::string> matched_keywords; // 匹配的关键词
// 构造函数
SearchResultItem() :
match_score(0.0),
is_in_session(false),
is_assembly(false),
component_count(0)
{
}
};
// 搜索结果结构
struct ModelSearchResult {
bool success; // 搜索是否成功
std::vector<SearchResultItem> 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<SearchResultItem>& results);
void SearchCurrentModel(const ModelSearchRequest& request, std::vector<SearchResultItem>& results);
void SearchFromRootAssemblies(const ModelSearchRequest& request, std::vector<SearchResultItem>& results);
void SearchAssemblyComponents(const ModelSearchRequest& request,
pfcAssembly_ptr assembly,
std::vector<SearchResultItem>& results,
std::set<std::string>& processed_models,
const std::string& parent_path = "");
void SearchAssemblyComponentsWithDedup(const ModelSearchRequest& request,
pfcAssembly_ptr assembly,
std::map<std::string, SearchResultItem>& best_results,
std::set<std::string>& 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<SearchResultItem>& 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<std::string>& type_filters);
// 字符串处理工具
static std::string ToLowerCase(const std::string& str);
static std::vector<std::string> 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<std::string> processed_models_;
// 性能计时
std::chrono::high_resolution_clock::time_point search_start_time_;
void StartTimer();
std::string GetElapsedTimeMs();
};

379
ModelSearchHandler.cpp Normal file
View File

@ -0,0 +1,379 @@
#include "pch.h"
#include "ModelSearchHandler.h"
#include <algorithm>
#include <cctype>
// 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<std::string> 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<std::string> ModelSearchHandler::ExtractJsonArrayValue(const std::string& json, const std::string& key) {
std::vector<std::string> 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<unsigned char>(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;
}

38
ModelSearchHandler.h Normal file
View File

@ -0,0 +1,38 @@
#pragma once
#include "ModelSearchEngine.h"
#include "HttpServer.h"
#include <string>
#include <sstream>
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<std::string> 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);
};